diff --git a/internal/clang/clang_test.go b/internal/clang/clang_test.go new file mode 100644 index 00000000..b998f977 --- /dev/null +++ b/internal/clang/clang_test.go @@ -0,0 +1,510 @@ +/* + * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package clang + +import ( + "bytes" + "os" + "reflect" + "strings" + "testing" +) + +func TestConfig(t *testing.T) { + t.Run("NewConfig", func(t *testing.T) { + ccflags := []string{"-O2", "-g"} + cflags := []string{"-std=c99"} + ldflags := []string{"-lm"} + + config := NewConfig("clang++", ccflags, cflags, ldflags, "ld.lld") + + if config.CC != "clang++" { + t.Errorf("Expected CC to be 'clang++', got %q", config.CC) + } + if !reflect.DeepEqual(config.CCFLAGS, ccflags) { + t.Errorf("Expected CCFLAGS %v, got %v", ccflags, config.CCFLAGS) + } + if !reflect.DeepEqual(config.CFLAGS, cflags) { + t.Errorf("Expected CFLAGS %v, got %v", cflags, config.CFLAGS) + } + if !reflect.DeepEqual(config.LDFLAGS, ldflags) { + t.Errorf("Expected LDFLAGS %v, got %v", ldflags, config.LDFLAGS) + } + if config.Linker != "ld.lld" { + t.Errorf("Expected Linker to be 'ld.lld', got %q", config.Linker) + } + }) +} + +func TestNew(t *testing.T) { + t.Run("WithApp", func(t *testing.T) { + config := Config{CC: "gcc"} + cmd := New("clang++", config) + + if cmd.app != "clang++" { + t.Errorf("Expected app to be 'clang++', got %q", cmd.app) + } + if !reflect.DeepEqual(cmd.config, config) { + t.Errorf("Expected config %+v, got %+v", config, cmd.config) + } + if cmd.Stdout != os.Stdout { + t.Error("Expected Stdout to be os.Stdout") + } + if cmd.Stderr != os.Stderr { + t.Error("Expected Stderr to be os.Stderr") + } + }) + + t.Run("WithoutApp", func(t *testing.T) { + config := Config{} + cmd := New("", config) + + if cmd.app != "clang" { + t.Errorf("Expected app to be 'clang', got %q", cmd.app) + } + }) +} + +func TestNewCompiler(t *testing.T) { + t.Run("WithCC", func(t *testing.T) { + config := Config{CC: "gcc"} + cmd := NewCompiler(config) + + if cmd.app != "gcc" { + t.Errorf("Expected app to be 'gcc', got %q", cmd.app) + } + }) + + t.Run("WithoutCC", func(t *testing.T) { + config := Config{} + cmd := NewCompiler(config) + + if cmd.app != "clang" { + t.Errorf("Expected app to be 'clang', got %q", cmd.app) + } + }) +} + +func TestNewLinker(t *testing.T) { + t.Run("WithLinker", func(t *testing.T) { + config := Config{Linker: "ld.lld"} + cmd := NewLinker(config) + + if cmd.app != "ld.lld" { + t.Errorf("Expected app to be 'ld.lld', got %q", cmd.app) + } + }) + + t.Run("WithoutLinkerButWithCC", func(t *testing.T) { + config := Config{CC: "gcc"} + cmd := NewLinker(config) + + if cmd.app != "gcc" { + t.Errorf("Expected app to be 'gcc', got %q", cmd.app) + } + }) + + t.Run("WithoutLinkerAndCC", func(t *testing.T) { + config := Config{} + cmd := NewLinker(config) + + if cmd.app != "clang" { + t.Errorf("Expected app to be 'clang', got %q", cmd.app) + } + }) +} + +func TestMergeCompilerFlags(t *testing.T) { + // Save original environment + origCCFlags := os.Getenv("CCFLAGS") + origCFlags := os.Getenv("CFLAGS") + defer func() { + os.Setenv("CCFLAGS", origCCFlags) + os.Setenv("CFLAGS", origCFlags) + }() + + t.Run("WithEnvironmentAndConfig", func(t *testing.T) { + os.Setenv("CCFLAGS", "-O2 -g") + os.Setenv("CFLAGS", "-std=c99") + + config := Config{ + CCFLAGS: []string{"-Wall"}, + CFLAGS: []string{"-pedantic"}, + } + cmd := New("clang", config) + + flags := cmd.mergeCompilerFlags() + expected := []string{"-O2", "-g", "-std=c99", "-Wall", "-pedantic"} + + if !reflect.DeepEqual(flags, expected) { + t.Errorf("Expected flags %v, got %v", expected, flags) + } + }) + + t.Run("WithOnlyConfig", func(t *testing.T) { + os.Unsetenv("CCFLAGS") + os.Unsetenv("CFLAGS") + + config := Config{ + CCFLAGS: []string{"-Wall"}, + CFLAGS: []string{"-pedantic"}, + } + cmd := New("clang", config) + + flags := cmd.mergeCompilerFlags() + expected := []string{"-Wall", "-pedantic"} + + if !reflect.DeepEqual(flags, expected) { + t.Errorf("Expected flags %v, got %v", expected, flags) + } + }) + + t.Run("WithOnlyEnvironment", func(t *testing.T) { + os.Setenv("CCFLAGS", "-O3") + os.Setenv("CFLAGS", "-fPIC") + + config := Config{} + cmd := New("clang", config) + + flags := cmd.mergeCompilerFlags() + expected := []string{"-O3", "-fPIC"} + + if !reflect.DeepEqual(flags, expected) { + t.Errorf("Expected flags %v, got %v", expected, flags) + } + }) + + t.Run("Empty", func(t *testing.T) { + os.Unsetenv("CCFLAGS") + os.Unsetenv("CFLAGS") + + config := Config{} + cmd := New("clang", config) + + flags := cmd.mergeCompilerFlags() + + if len(flags) != 0 { + t.Errorf("Expected empty flags, got %v", flags) + } + }) +} + +func TestMergeLinkerFlags(t *testing.T) { + // Save original environment + origCCFlags := os.Getenv("CCFLAGS") + origLDFlags := os.Getenv("LDFLAGS") + defer func() { + os.Setenv("CCFLAGS", origCCFlags) + os.Setenv("LDFLAGS", origLDFlags) + }() + + t.Run("WithEnvironmentAndConfig", func(t *testing.T) { + os.Setenv("CCFLAGS", "-O2") + os.Setenv("LDFLAGS", "-lm -lpthread") + + config := Config{ + LDFLAGS: []string{"-static"}, + } + cmd := New("clang", config) + + flags := cmd.mergeLinkerFlags() + expected := []string{"-O2", "-lm", "-lpthread", "-static"} + + if !reflect.DeepEqual(flags, expected) { + t.Errorf("Expected flags %v, got %v", expected, flags) + } + }) + + t.Run("WithOnlyConfig", func(t *testing.T) { + os.Unsetenv("CCFLAGS") + os.Unsetenv("LDFLAGS") + + config := Config{ + LDFLAGS: []string{"-static", "-lm"}, + } + cmd := New("clang", config) + + flags := cmd.mergeLinkerFlags() + expected := []string{"-static", "-lm"} + + if !reflect.DeepEqual(flags, expected) { + t.Errorf("Expected flags %v, got %v", expected, flags) + } + }) +} + +func TestCompile(t *testing.T) { + // This test uses echo instead of clang to avoid dependency on clang installation + config := Config{ + CCFLAGS: []string{"-Wall"}, + CFLAGS: []string{"-std=c99"}, + } + cmd := New("echo", config) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + err := cmd.Compile("-c", "test.c") + if err != nil { + t.Errorf("Compile failed: %v", err) + } + + output := strings.TrimSpace(stdout.String()) + expectedArgs := "-Wall -std=c99 -c test.c" + if output != expectedArgs { + t.Errorf("Expected output %q, got %q", expectedArgs, output) + } +} + +func TestLink(t *testing.T) { + // This test uses echo instead of clang to avoid dependency on clang installation + config := Config{ + LDFLAGS: []string{"-lm"}, + } + cmd := New("echo", config) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + err := cmd.Link("test.o", "-o", "test") + if err != nil { + t.Errorf("Link failed: %v", err) + } + + output := strings.TrimSpace(stdout.String()) + expectedArgs := "-lm test.o -o test" + if output != expectedArgs { + t.Errorf("Expected output %q, got %q", expectedArgs, output) + } +} + +func TestVerboseMode(t *testing.T) { + config := Config{} + cmd := New("echo", config) + cmd.Verbose = true + + // Since verbose output goes to os.Stderr directly, we'll test + // that verbose flag is set and command executes successfully + var stderr bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &bytes.Buffer{} // Suppress stdout + + err := cmd.Compile("-c", "test.c") + if err != nil { + t.Errorf("Compile failed: %v", err) + } + + // The test passes if verbose is set and no error occurred + if !cmd.Verbose { + t.Error("Expected Verbose to be true") + } +} + +func TestCmdEnvironment(t *testing.T) { + config := Config{} + cmd := New("echo", config) + cmd.Env = []string{"TEST_VAR=test_value"} + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + // Use a command that will show environment variables + // Note: This is a simplified test that just ensures the Env field is set + err := cmd.Compile("-c", "test.c") + if err != nil { + t.Errorf("Compile failed: %v", err) + } + + if len(cmd.Env) != 1 || cmd.Env[0] != "TEST_VAR=test_value" { + t.Errorf("Expected environment to be set correctly") + } +} + +func TestCmdIO(t *testing.T) { + config := Config{} + cmd := New("cat", config) // Use cat to test stdin/stdout + + input := "test input" + cmd.Stdin = strings.NewReader(input) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + // cat will read from stdin and write to stdout + err := cmd.exec() // Call exec directly with no args + if err != nil { + t.Errorf("exec failed: %v", err) + } + + output := stdout.String() + if output != input { + t.Errorf("Expected output %q, got %q", input, output) + } +} + +func TestCheckLinkArgs(t *testing.T) { + t.Run("BasicTest", func(t *testing.T) { + // Use echo instead of clang to avoid dependency + config := Config{} + cmd := New("echo", config) + + // Redirect output to avoid clutter + cmd.Stdout = &bytes.Buffer{} + cmd.Stderr = &bytes.Buffer{} + + // This should succeed with echo + err := cmd.CheckLinkArgs([]string{"-o"}, false) + if err != nil { + t.Errorf("CheckLinkArgs failed: %v", err) + } + }) + + t.Run("WasmExtension", func(t *testing.T) { + config := Config{} + cmd := New("echo", config) + + cmd.Stdout = &bytes.Buffer{} + cmd.Stderr = &bytes.Buffer{} + + // Test with wasm=true + err := cmd.CheckLinkArgs([]string{"-o"}, true) + if err != nil { + t.Errorf("CheckLinkArgs with wasm failed: %v", err) + } + }) +} + +// Additional table-driven test for flag merging scenarios +func TestFlagMergingScenarios(t *testing.T) { + tests := []struct { + name string + envCCFlags string + envCFlags string + envLDFlags string + config Config + expectComp []string + expectLink []string + }{ + { + name: "empty everything", + envCCFlags: "", + envCFlags: "", + envLDFlags: "", + config: Config{}, + expectComp: []string{}, + expectLink: []string{}, + }, + { + name: "only environment flags", + envCCFlags: "-O2", + envCFlags: "-std=c99", + envLDFlags: "-lm", + config: Config{}, + expectComp: []string{"-O2", "-std=c99"}, + expectLink: []string{"-O2", "-lm"}, + }, + { + name: "only config flags", + envCCFlags: "", + envCFlags: "", + envLDFlags: "", + config: Config{ + CCFLAGS: []string{"-Wall"}, + CFLAGS: []string{"-pedantic"}, + LDFLAGS: []string{"-static"}, + }, + expectComp: []string{"-Wall", "-pedantic"}, + expectLink: []string{"-static"}, + }, + { + name: "mixed environment and config", + envCCFlags: "-O3", + envCFlags: "-fPIC", + envLDFlags: "-lm -lpthread", + config: Config{ + CCFLAGS: []string{"-Wall", "-Wextra"}, + CFLAGS: []string{"-std=c11"}, + LDFLAGS: []string{"-static"}, + }, + expectComp: []string{"-O3", "-fPIC", "-Wall", "-Wextra", "-std=c11"}, + expectLink: []string{"-O3", "-lm", "-lpthread", "-static"}, + }, + } + + // Save original environment + origCCFlags := os.Getenv("CCFLAGS") + origCFlags := os.Getenv("CFLAGS") + origLDFlags := os.Getenv("LDFLAGS") + defer func() { + os.Setenv("CCFLAGS", origCCFlags) + os.Setenv("CFLAGS", origCFlags) + os.Setenv("LDFLAGS", origLDFlags) + }() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set environment variables + os.Setenv("CCFLAGS", tt.envCCFlags) + os.Setenv("CFLAGS", tt.envCFlags) + os.Setenv("LDFLAGS", tt.envLDFlags) + + cmd := New("clang", tt.config) + + // Test compiler flags + compFlags := cmd.mergeCompilerFlags() + if len(tt.expectComp) == 0 && len(compFlags) == 0 { + // Both are empty, consider them equal + } else if !reflect.DeepEqual(compFlags, tt.expectComp) { + t.Errorf("mergeCompilerFlags() = %v, want %v", compFlags, tt.expectComp) + } + + // Test linker flags + linkFlags := cmd.mergeLinkerFlags() + if len(tt.expectLink) == 0 && len(linkFlags) == 0 { + // Both are empty, consider them equal + } else if !reflect.DeepEqual(linkFlags, tt.expectLink) { + t.Errorf("mergeLinkerFlags() = %v, want %v", linkFlags, tt.expectLink) + } + }) + } +} + +// Benchmark tests +func BenchmarkMergeCompilerFlags(b *testing.B) { + config := Config{ + CCFLAGS: []string{"-Wall", "-O2"}, + CFLAGS: []string{"-std=c99", "-pedantic"}, + } + cmd := New("clang", config) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = cmd.mergeCompilerFlags() + } +} + +func BenchmarkMergeLinkerFlags(b *testing.B) { + config := Config{ + LDFLAGS: []string{"-lm", "-lpthread", "-static"}, + } + cmd := New("clang", config) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = cmd.mergeLinkerFlags() + } +}