-
Notifications
You must be signed in to change notification settings - Fork 46
cltest: combine IR and runtime golden checks #1771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
33baf29
e4070a5
c2090b2
be960b0
fcbe9fb
b3bc372
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,7 @@ import ( | |
| "github.com/goplus/llgo/internal/llgen" | ||
| "github.com/goplus/llgo/internal/mockable" | ||
| "github.com/goplus/llgo/ssa/ssatest" | ||
| gllvm "github.com/goplus/llvm" | ||
| "github.com/qiniu/x/test" | ||
| "golang.org/x/tools/go/ssa" | ||
| "golang.org/x/tools/go/ssa/ssautil" | ||
|
|
@@ -79,11 +80,12 @@ func FromDir(t *testing.T, sel, relDir string) { | |
| } | ||
|
|
||
| type runOptions struct { | ||
| conf *build.Config | ||
| filter func(string) string | ||
| conf *build.Config | ||
| filter func(string) string | ||
| checkIR bool | ||
| } | ||
|
|
||
| // RunOption customizes RunFromDir behavior. | ||
| // RunOption customizes directory-based test behavior. | ||
| type RunOption func(*runOptions) | ||
|
|
||
| // WithRunConfig uses the provided build config for test runs. | ||
|
|
@@ -100,6 +102,13 @@ func WithOutputFilter(filter func(string) string) RunOption { | |
| } | ||
| } | ||
|
|
||
| // WithIRCheck enables or disables IR golden checks in RunAndTestFromDir. | ||
| func WithIRCheck(enabled bool) RunOption { | ||
| return func(opts *runOptions) { | ||
| opts.checkIR = enabled | ||
| } | ||
| } | ||
|
|
||
| // FilterEmulatorOutput strips emulator boot logs by returning output after "entry 0x...". | ||
| func FilterEmulatorOutput(output string) string { | ||
| output = strings.ReplaceAll(output, "\r\n", "\n") | ||
|
|
@@ -117,9 +126,10 @@ func FilterEmulatorOutput(output string) string { | |
| return output | ||
| } | ||
|
|
||
| // RunFromDir executes tests under relDir, skipping any relPkg entries in ignore. | ||
| // ignore entries should be relative package paths (e.g., "./_testgo/invoke"). | ||
| func RunFromDir(t *testing.T, sel, relDir string, ignore []string, opts ...RunOption) { | ||
| // RunAndTestFromDir executes tests under relDir and validates both runtime | ||
| // output and the pre-transform package IR snapshot when the corresponding | ||
| // golden files exist. | ||
| func RunAndTestFromDir(t *testing.T, sel, relDir string, ignore []string, opts ...RunOption) { | ||
| rootDir, err := os.Getwd() | ||
| if err != nil { | ||
| t.Fatal("Getwd failed:", err) | ||
|
|
@@ -129,7 +139,7 @@ func RunFromDir(t *testing.T, sel, relDir string, ignore []string, opts ...RunOp | |
| for _, item := range ignore { | ||
| ignoreSet[item] = struct{}{} | ||
| } | ||
| options := runOptions{} | ||
| options := runOptions{checkIR: true} | ||
| for _, opt := range opts { | ||
| opt(&options) | ||
| } | ||
|
|
@@ -155,7 +165,7 @@ func RunFromDir(t *testing.T, sel, relDir string, ignore []string, opts ...RunOp | |
| continue | ||
| } | ||
| t.Run(name, func(t *testing.T) { | ||
| testRunFrom(t, pkgDir, relPkg, sel, options) | ||
| testRunAndTestFrom(t, pkgDir, relPkg, sel, options) | ||
| }) | ||
| } | ||
| } | ||
|
|
@@ -211,37 +221,62 @@ func testFrom(t *testing.T, pkgDir, sel string) { | |
| } | ||
| } | ||
|
|
||
| func testRunFrom(t *testing.T, pkgDir, relPkg, sel string, opts runOptions) { | ||
| func testRunAndTestFrom(t *testing.T, pkgDir, relPkg, sel string, opts runOptions) { | ||
| if sel != "" && !strings.Contains(pkgDir, sel) { | ||
| return | ||
| } | ||
| expectedPath := filepath.Join(pkgDir, "expect.txt") | ||
| expected, err := os.ReadFile(expectedPath) | ||
|
|
||
| expectedOutput, checkOutput, err := readGolden(filepath.Join(pkgDir, "expect.txt")) | ||
| if err != nil { | ||
| if errors.Is(err, os.ErrNotExist) { | ||
| return | ||
| } | ||
| t.Fatal("ReadFile failed:", err) | ||
| } | ||
| if bytes.Equal(expected, []byte{';'}) { // expected == ";" means skipping expect.txt | ||
| if !checkOutput { | ||
| // IR-only mode: when expect.txt is not checked, use llgen.GenFrom via | ||
| // testFrom to compare this package's generated IR against out.ll. | ||
| if opts.checkIR { | ||
| testFrom(t, pkgDir, sel) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| var output []byte | ||
| if opts.conf != nil { | ||
| output, err = RunAndCaptureWithConf(relPkg, pkgDir, opts.conf) | ||
| } else { | ||
| output, err = RunAndCapture(relPkg, pkgDir) | ||
| var goldenIR []byte | ||
| checkIR := false | ||
| moduleID := "" | ||
| if opts.checkIR { | ||
| goldenIR, checkIR, err = readGolden(filepath.Join(pkgDir, "out.ll")) | ||
| if err != nil { | ||
| t.Fatal("ReadFile failed:", err) | ||
| } | ||
| if checkIR { | ||
| moduleID = moduleIDFromIR(goldenIR) | ||
| if moduleID == "" { | ||
| t.Fatalf("missing ModuleID in golden IR for %s", pkgDir) | ||
| } | ||
| } | ||
| } | ||
| conf := opts.conf | ||
| var capturedIR *string | ||
| if checkIR { | ||
| conf, capturedIR = withModuleCapture(opts.conf, moduleID) | ||
| } | ||
|
|
||
| output, err := runWithConf(relPkg, pkgDir, conf) | ||
| if err != nil { | ||
| t.Logf("raw output:\n%s", string(output)) | ||
| t.Fatalf("run failed: %v\noutput: %s", err, string(output)) | ||
| } | ||
| if opts.filter != nil { | ||
| output = []byte(opts.filter(string(output))) | ||
|
|
||
| if checkOutput { | ||
| assertExpectedOutput(t, pkgDir, expectedOutput, output, opts) | ||
| } | ||
| if test.Diff(t, filepath.Join(pkgDir, "expect.txt.new"), output, expected) { | ||
| t.Fatal("unexpected output") | ||
| if !checkIR { | ||
| return | ||
| } | ||
| if capturedIR == nil || *capturedIR == "" { | ||
| t.Fatalf("module snapshot missing for package %s", moduleID) | ||
| } | ||
| if test.Diff(t, filepath.Join(pkgDir, "result.txt"), []byte(*capturedIR), goldenIR) { | ||
| t.Fatal("unexpected IR output") | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -252,6 +287,31 @@ func RunAndCapture(relPkg, pkgDir string) ([]byte, error) { | |
|
|
||
| // RunAndCaptureWithConf runs llgo with a custom build config and captures output. | ||
| func RunAndCaptureWithConf(relPkg, pkgDir string, conf *build.Config) ([]byte, error) { | ||
| return runWithConf(relPkg, pkgDir, conf) | ||
| } | ||
|
|
||
| func withModuleCapture(conf *build.Config, targetPkgPath string) (*build.Config, *string) { | ||
| if conf == nil { | ||
| conf = build.NewDefaultConf(build.ModeRun) | ||
| } | ||
| localConf := *conf | ||
| var module string | ||
| prevHook := localConf.ModuleHook | ||
| localConf.ModuleHook = func(pkgPath string, mod gllvm.Module) { | ||
| if prevHook != nil { | ||
| prevHook(pkgPath, mod) | ||
| } | ||
| if pkgPath == targetPkgPath && module == "" { | ||
| module = mod.String() | ||
| } | ||
| } | ||
| return &localConf, &module | ||
| } | ||
|
|
||
| func runWithConf(relPkg, pkgDir string, conf *build.Config) ([]byte, error) { | ||
| if conf == nil { | ||
| conf = build.NewDefaultConf(build.ModeRun) | ||
| } | ||
| cacheDir, err := os.MkdirTemp("", "llgo-gocache-*") | ||
| if err != nil { | ||
| return nil, err | ||
|
|
@@ -269,9 +329,6 @@ func RunAndCaptureWithConf(relPkg, pkgDir string, conf *build.Config) ([]byte, e | |
| } | ||
| }() | ||
|
|
||
| if conf == nil { | ||
| return nil, fmt.Errorf("build config is nil") | ||
| } | ||
| localConf := *conf | ||
|
|
||
| originalStdout := os.Stdout | ||
|
|
@@ -307,11 +364,6 @@ func RunAndCaptureWithConf(relPkg, pkgDir string, conf *build.Config) ([]byte, e | |
| } | ||
| defer os.Chdir(origDir) | ||
| relPkg = "." | ||
| if _, err := os.Stat(filepath.Join(pkgDir, "in.go")); err == nil { | ||
| relPkg = "in.go" | ||
| } else if _, err := os.Stat(filepath.Join(pkgDir, "main.go")); err == nil { | ||
| relPkg = "main.go" | ||
| } | ||
| } | ||
|
|
||
| mockable.EnableMock() | ||
|
|
@@ -339,6 +391,43 @@ func RunAndCaptureWithConf(relPkg, pkgDir string, conf *build.Config) ([]byte, e | |
| return output, nil | ||
| } | ||
|
|
||
| func assertExpectedOutput(t *testing.T, pkgDir string, expectedOutput, output []byte, opts runOptions) { | ||
| t.Helper() | ||
| if opts.filter != nil { | ||
| output = []byte(opts.filter(string(output))) | ||
| } | ||
| if test.Diff(t, filepath.Join(pkgDir, "expect.txt.new"), output, expectedOutput) { | ||
| t.Fatal("unexpected output") | ||
| } | ||
| } | ||
|
|
||
| func readGolden(file string) ([]byte, bool, error) { | ||
| data, err := os.ReadFile(file) | ||
| if err != nil { | ||
| if errors.Is(err, os.ErrNotExist) { | ||
| return nil, false, nil | ||
| } | ||
| return nil, false, err | ||
| } | ||
| if bytes.Equal(data, []byte{';'}) { | ||
| return data, false, nil | ||
| } | ||
| return data, true, nil | ||
| } | ||
|
|
||
| func moduleIDFromIR(data []byte) string { | ||
| lines := strings.Split(string(data), "\n") | ||
| if len(lines) == 0 { | ||
| return "" | ||
| } | ||
| const prefix = "; ModuleID = '" | ||
| line := strings.TrimSpace(lines[0]) | ||
| if !strings.HasPrefix(line, prefix) || !strings.HasSuffix(line, "'") { | ||
| return "" | ||
| } | ||
| return strings.TrimSuffix(strings.TrimPrefix(line, prefix), "'") | ||
|
Comment on lines
+419
to
+428
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
References
|
||
| } | ||
|
|
||
| func filterRunOutput(in []byte) []byte { | ||
| // Tests compare output with expect.txt. Some toolchain/environment warnings are | ||
| // inherently machine-specific and should not be part of the golden output. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -162,18 +162,15 @@ func runEmbedTargetSuite(t *testing.T, target, relDir string, ignore []string) { | |
| conf := build.NewDefaultConf(build.ModeRun) | ||
| conf.Target = target | ||
| conf.Emulator = true | ||
| cltest.RunFromDir(t, "", relDir, ignore, | ||
| cltest.RunAndTestFromDir(t, "", relDir, ignore, | ||
| cltest.WithRunConfig(conf), | ||
| cltest.WithOutputFilter(cltest.FilterEmulatorOutput), | ||
| cltest.WithIRCheck(false), | ||
| ) | ||
| } | ||
|
|
||
| func TestFromTestgo(t *testing.T) { | ||
| cltest.FromDir(t, "", "./_testgo") | ||
| } | ||
|
|
||
| func TestRunFromTestgo(t *testing.T) { | ||
| cltest.RunFromDir(t, "", "./_testgo", nil) | ||
| func TestRunAndTestFromTestgo(t *testing.T) { | ||
| cltest.RunAndTestFromDir(t, "", "./_testgo", nil) | ||
| } | ||
|
|
||
| func TestFilterEmulatorOutput(t *testing.T) { | ||
|
|
@@ -254,57 +251,37 @@ func TestRunFromTestgoSelectAllowsKnownInterleavings(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| func TestFromTestpy(t *testing.T) { | ||
| cltest.FromDir(t, "", "./_testpy") | ||
| } | ||
|
|
||
| func TestRunFromTestpy(t *testing.T) { | ||
| cltest.RunFromDir(t, "", "./_testpy", nil) | ||
| } | ||
|
|
||
| func TestFromTestlibgo(t *testing.T) { | ||
| cltest.FromDir(t, "", "./_testlibgo") | ||
| func TestRunAndTestFromTestpy(t *testing.T) { | ||
| cltest.RunAndTestFromDir(t, "", "./_testpy", nil) | ||
| } | ||
|
|
||
| func TestRunFromTestlibgo(t *testing.T) { | ||
| cltest.RunFromDir(t, "", "./_testlibgo", nil) | ||
| func TestRunAndTestFromTestlibgo(t *testing.T) { | ||
| cltest.RunAndTestFromDir(t, "", "./_testlibgo", nil) | ||
| } | ||
|
|
||
| func TestFromTestlibc(t *testing.T) { | ||
| cltest.FromDir(t, "", "./_testlibc") | ||
| } | ||
|
|
||
| func TestRunFromTestlibc(t *testing.T) { | ||
| func TestRunAndTestFromTestlibc(t *testing.T) { | ||
| var ignore []string | ||
| if runtime.GOOS == "linux" { | ||
| ignore = []string{ | ||
| "./_testlibc/demangle", // Linux demangle symbol differs (itaniumDemangle linkage mismatch). | ||
| } | ||
| } | ||
| cltest.RunFromDir(t, "", "./_testlibc", ignore) | ||
| } | ||
|
|
||
| func TestFromTestrt(t *testing.T) { | ||
| cltest.FromDir(t, "", "./_testrt") | ||
| cltest.RunAndTestFromDir(t, "", "./_testlibc", ignore) | ||
| } | ||
|
|
||
| func TestRunFromTestrt(t *testing.T) { | ||
| func TestRunAndTestFromTestrt(t *testing.T) { | ||
| var ignore []string | ||
| if runtime.GOOS == "linux" { | ||
| ignore = []string{ | ||
| "./_testrt/asmfull", // Output is macOS-specific. | ||
|
Comment on lines
+272
to
276
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original |
||
| "./_testrt/fprintf", // Linux uses different stderr symbol (no __stderrp). | ||
| } | ||
| } | ||
| cltest.RunFromDir(t, "", "./_testrt", ignore) | ||
| } | ||
|
|
||
| func TestFromTestdata(t *testing.T) { | ||
| cltest.FromDir(t, "", "./_testdata") | ||
| cltest.RunAndTestFromDir(t, "", "./_testrt", ignore) | ||
| } | ||
|
|
||
| func TestRunFromTestdata(t *testing.T) { | ||
| cltest.RunFromDir(t, "", "./_testdata", nil) | ||
| func TestRunAndTestFromTestdata(t *testing.T) { | ||
| cltest.RunAndTestFromDir(t, "", "./_testdata", nil) | ||
| } | ||
|
|
||
| func TestCgofullGeneratesC2func(t *testing.T) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function duplicates the directory traversal, ignore set handling, and test selection logic from
RunFromDir. Consider refactoring the common logic into a shared helper function to improve maintainability and reduce code duplication.