| // Copyright 2025 Google LLC | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| package main | |
| import ( | |
| "testing" | |
| "time" | |
| "dawn.googlesource.com/dawn/tools/src/container" | |
| "dawn.googlesource.com/dawn/tools/src/oswrapper" | |
| "github.com/stretchr/testify/require" | |
| ) | |
| // invoke() and things that call it cannot be unittested due to the call to | |
| // exec.CommandContext(). | |
| func TestExtractValidationHashes(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| input string | |
| wantString string | |
| wantHashes []string | |
| }{ | |
| { | |
| name: "No matches", | |
| input: "ASDF", | |
| wantString: "ASDF", | |
| wantHashes: nil, | |
| }, | |
| { | |
| name: "Single match", | |
| input: "Line 1\n<<HASH: ASDF>>\nLine 3", | |
| wantString: "Line 1\nLine 3", | |
| wantHashes: []string{"ASDF"}, | |
| }, | |
| { | |
| name: "Multiple matches", | |
| input: "Line 1\n<<HASH: ASDF>>\nLine 3\n<<HASH: QWER>>\nLine 5", | |
| wantString: "Line 1\nLine 3\nLine 5", | |
| wantHashes: []string{"ASDF", "QWER"}, | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| out, hashes := extractValidationHashes(testCase.input) | |
| require.Equal(t, testCase.wantString, out) | |
| require.Equal(t, testCase.wantHashes, hashes) | |
| }) | |
| } | |
| } | |
| func TestIndent(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| input string | |
| indentAmount int | |
| want string | |
| }{ | |
| { | |
| name: "No indent single line", | |
| input: "Foo", | |
| indentAmount: 0, | |
| want: "Foo", | |
| }, | |
| { | |
| name: "Indent single line", | |
| input: "Foo", | |
| indentAmount: 2, | |
| want: " Foo", | |
| }, | |
| { | |
| name: "No indent multi line", | |
| input: "Foo\nBar", | |
| indentAmount: 0, | |
| want: "Foo\nBar", | |
| }, | |
| { | |
| name: "Indent multi line", | |
| input: "Foo\nBar", | |
| indentAmount: 2, | |
| want: " Foo\n Bar", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| require.Equal(t, testCase.want, indent(testCase.input, testCase.indentAmount)) | |
| }) | |
| } | |
| } | |
| func TestAlignLeft(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| input string | |
| width int | |
| want string | |
| }{ | |
| { | |
| name: "Width smaller than string length", | |
| input: "Foo", | |
| width: 2, | |
| want: "Foo", | |
| }, | |
| { | |
| name: "Width equal to string length", | |
| input: "Foo", | |
| width: 3, | |
| want: "Foo", | |
| }, | |
| { | |
| name: "Width greater than string length", | |
| input: "Foo", | |
| width: 5, | |
| want: "Foo ", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| require.Equal(t, testCase.want, alignLeft(testCase.input, testCase.width)) | |
| }) | |
| } | |
| } | |
| func TestAlignCenter(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| input string | |
| width int | |
| want string | |
| }{ | |
| { | |
| name: "Width smaller than string length", | |
| input: "Foo", | |
| width: 2, | |
| want: "Foo", | |
| }, | |
| { | |
| name: "Width equal to string length", | |
| input: "Foo", | |
| width: 3, | |
| want: "Foo", | |
| }, | |
| { | |
| name: "Width greater than string length", | |
| input: "Foo", | |
| width: 5, | |
| want: " Foo ", | |
| }, | |
| { | |
| name: "Width greater than string length non-equal padding", | |
| input: "Foo", | |
| width: 6, | |
| want: " Foo ", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| require.Equal(t, testCase.want, alignCenter(testCase.input, testCase.width)) | |
| }) | |
| } | |
| } | |
| func TestAlignRight(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| input string | |
| width int | |
| want string | |
| }{ | |
| { | |
| name: "Width smaller than string length", | |
| input: "Foo", | |
| width: 2, | |
| want: "Foo", | |
| }, | |
| { | |
| name: "Width equal to string length", | |
| input: "Foo", | |
| width: 3, | |
| want: "Foo", | |
| }, | |
| { | |
| name: "Width greater than string length", | |
| input: "Foo", | |
| width: 5, | |
| want: " Foo", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| require.Equal(t, testCase.want, alignRight(testCase.input, testCase.width)) | |
| }) | |
| } | |
| } | |
| func TestMaxStringLen(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| input []string | |
| want int | |
| }{ | |
| { | |
| name: "Nil", | |
| input: nil, | |
| want: 0, | |
| }, | |
| { | |
| name: "Empty", | |
| input: []string{}, | |
| want: 0, | |
| }, | |
| { | |
| name: "Multiple lines", | |
| input: []string{ | |
| "Line 1", | |
| "AAAAAAAAAAAA", | |
| "Line 3", | |
| }, | |
| want: 12, | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| require.Equal(t, testCase.want, maxStringLen(testCase.input)) | |
| }) | |
| } | |
| } | |
| func TestFormatWidth(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| input outputFormat | |
| want int | |
| }{ | |
| { | |
| name: "Less than minimum", | |
| input: "", | |
| want: 6, | |
| }, | |
| { | |
| name: "Equal to minimum", | |
| input: "123456", | |
| want: 6, | |
| }, | |
| { | |
| name: "Greater than minimum", | |
| input: "1234567890", | |
| want: 10, | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| require.Equal(t, testCase.want, formatWidth(testCase.input)) | |
| }) | |
| } | |
| } | |
| func TestPercentage(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| inputN int | |
| inputTotal int | |
| want string | |
| }{ | |
| { | |
| name: "Zero total", | |
| inputN: 10, | |
| inputTotal: 0, | |
| want: "-", | |
| }, | |
| { | |
| name: "Zero N", | |
| inputN: 0, | |
| inputTotal: 10, | |
| want: "0.0%", | |
| }, | |
| { | |
| name: "Both positive", | |
| inputN: 1, | |
| inputTotal: 10, | |
| want: "10.0%", | |
| }, | |
| { | |
| name: "Negative N", | |
| inputN: -1, | |
| inputTotal: 10, | |
| want: "-10.0%", | |
| }, | |
| { | |
| name: "Negative total", | |
| inputN: 1, | |
| inputTotal: -10, | |
| want: "-10.0%", | |
| }, | |
| { | |
| name: "Both negative", | |
| inputN: -1, | |
| inputTotal: -10, | |
| want: "10.0%", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| require.Equal(t, testCase.want, percentage(testCase.inputN, testCase.inputTotal)) | |
| }) | |
| } | |
| } | |
| func TestParseFlags(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| path string | |
| contents string | |
| skipFileWrite bool | |
| want []cmdLineFlags | |
| wantErr bool | |
| wantErrMsg string | |
| }{ | |
| { | |
| name: "Non-existent file", | |
| path: "/foo.txt", | |
| skipFileWrite: true, | |
| want: nil, | |
| wantErr: true, | |
| wantErrMsg: "open /foo.txt: file does not exist", | |
| }, | |
| { | |
| name: "Single entry single flag no format", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // flags: --hlsl-shader-model 60 | |
| // Another comment | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(allOutputFormats...), | |
| flags: []string{"", "--hlsl-shader-model", "60"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Single entry multiple flags no format", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // flags: --hlsl-shader-model 60 --overrides wgsize=10 | |
| // Another comment | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(allOutputFormats...), | |
| flags: []string{"", "--hlsl-shader-model", "60", "--overrides", "wgsize=10"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Single entry single flag with format", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // [wgsl] flags: --hlsl-shader-model 60 | |
| // Another comment | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(wgsl), | |
| flags: []string{"", "--hlsl-shader-model", "60"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Single entry multiple flags with format", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // [wgsl] flags: --hlsl-shader-model 60 --overrides wgsize=10 | |
| // Another comment | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(wgsl), | |
| flags: []string{"", "--hlsl-shader-model", "60", "--overrides", "wgsize=10"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Multiple entries single flag no format", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // flags: --hlsl-shader-model 60 | |
| // Another comment | |
| // flags: --overrides wgsize=10 | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(allOutputFormats...), | |
| flags: []string{"", "--hlsl-shader-model", "60"}, | |
| }, | |
| { | |
| formats: container.NewSet(allOutputFormats...), | |
| flags: []string{"", "--overrides", "wgsize=10"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Multiple entries multiple flags no format", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // flags: --hlsl-shader-model 60 --foo | |
| // Another comment | |
| // flags: --overrides wgsize=10 --bar | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(allOutputFormats...), | |
| flags: []string{"", "--hlsl-shader-model", "60", "--foo"}, | |
| }, | |
| { | |
| formats: container.NewSet(allOutputFormats...), | |
| flags: []string{"", "--overrides", "wgsize=10", "--bar"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Multiple entries single flag with format", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // [wgsl] flags: --hlsl-shader-model 60 | |
| // Another comment | |
| // [glsl] flags: --overrides wgsize=10 | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(wgsl), | |
| flags: []string{"", "--hlsl-shader-model", "60"}, | |
| }, | |
| { | |
| formats: container.NewSet(glsl), | |
| flags: []string{"", "--overrides", "wgsize=10"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Multiple entries multiple flags with format", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // [wgsl] flags: --hlsl-shader-model 60 --foo | |
| // Another comment | |
| // [glsl] flags: --overrides wgsize=10 --bar | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(wgsl), | |
| flags: []string{"", "--hlsl-shader-model", "60", "--foo"}, | |
| }, | |
| { | |
| formats: container.NewSet(glsl), | |
| flags: []string{"", "--overrides", "wgsize=10", "--bar"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Multiple entries multiple flags mixed formats", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // flags: --hlsl-shader-model 60 --foo | |
| // Another comment | |
| // [glsl] flags: --overrides wgsize=10 --bar | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(allOutputFormats...), | |
| flags: []string{"", "--hlsl-shader-model", "60", "--foo"}, | |
| }, | |
| { | |
| formats: container.NewSet(glsl), | |
| flags: []string{"", "--overrides", "wgsize=10", "--bar"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Invalid format", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // [invalid] flags: --hlsl-shader-model 60 | |
| // Another comment | |
| `, | |
| want: nil, | |
| wantErr: true, | |
| wantErrMsg: "unknown format 'invalid'", | |
| }, | |
| { | |
| name: "-ir formats automatically included", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // [hlsl-dxc] flags: --hlsl-shader-model 60 | |
| // Another comment | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(hlslDXC), | |
| flags: []string{"", "--hlsl-shader-model", "60"}, | |
| }, | |
| }, | |
| }, | |
| { | |
| name: "Ignored after first comment block", | |
| path: "/foo.txt", | |
| contents: `// Comment | |
| // [wgsl] flags: --hlsl-shader-model 60 | |
| // Another comment | |
| foo | |
| // [glsl] flags: --hlsl-shader-model 60 | |
| `, | |
| want: []cmdLineFlags{ | |
| { | |
| formats: container.NewSet(wgsl), | |
| flags: []string{"", "--hlsl-shader-model", "60"}, | |
| }, | |
| }, | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| wrapper := oswrapper.CreateFSTestOSWrapper() | |
| if !testCase.skipFileWrite { | |
| wrapper.WriteFile(testCase.path, []byte(testCase.contents), 0o700) | |
| } | |
| flags, err := parseFlags(testCase.path, wrapper) | |
| if testCase.wantErr { | |
| require.ErrorContains(t, err, testCase.wantErrMsg) | |
| } else { | |
| require.NoErrorf(t, err, "Got error parsing flags: %v", err) | |
| } | |
| require.Equal(t, testCase.want, flags) | |
| }) | |
| } | |
| } | |
| func TestPrintDuration(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| inputToParse string | |
| want string | |
| }{ | |
| { | |
| name: "Seconds", | |
| inputToParse: "5s", | |
| want: "5s", | |
| }, | |
| { | |
| name: "Minutes", | |
| inputToParse: "5m", | |
| want: "5m", | |
| }, | |
| { | |
| name: "Minutes and Seconds", | |
| inputToParse: "5m5s", | |
| want: "5m5s", | |
| }, | |
| { | |
| name: "Hours", | |
| inputToParse: "5h", | |
| want: "5h", | |
| }, | |
| { | |
| name: "Hours and Minutes", | |
| inputToParse: "5h5m", | |
| want: "5h5m", | |
| }, | |
| { | |
| name: "Hours and Seconds", | |
| inputToParse: "5h5s", | |
| want: "5h5s", | |
| }, | |
| { | |
| name: "Hours, Minutes, and Seconds", | |
| inputToParse: "5h5m5s", | |
| want: "5h5m5s", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| input, err := time.ParseDuration(testCase.inputToParse) | |
| require.NoErrorf(t, err, "Failed to parse duration: %v", err) | |
| require.Equal(t, testCase.want, printDuration(input)) | |
| }) | |
| } | |
| } |