From badde25fa972c94f7f8ce9398b8b21025ec2fbee Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 12 Aug 2026 15:51:50 +0900 Subject: [PATCH] test: cover driver-aware warm package reuse --- test/sep/localbuild_test.ww | 262 ++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) diff --git a/test/sep/localbuild_test.ww b/test/sep/localbuild_test.ww index e1f703ba..732e83e4 100644 --- a/test/sep/localbuild_test.ww +++ b/test/sep/localbuild_test.ww @@ -802,6 +802,268 @@ fn writediamond(td: str, reverse: bool) str = { testenv.clean(td); }; +// Unit bytes cannot identify driver-owned graph, archive, and commit semantics. +// Keep their executable content beside the compiler/assembler identities so a +// warm voucher never crosses a builder change. +@test fn persistent_build_reuse_tracks_driver_identity() void = { + let td: str = testenv.fresh(); + let source: str = strings.concat(td, "/source tree"); + let app: str = strings.concat(source, "/app"); + let dep: str = strings.concat(source, "/dep"); + let leaf: str = strings.concat(source, "/leaf"); + let tools: str = strings.concat(td, "/tool wrappers"); + let cdrivers: str = strings.concat(td, "/c driver copy"); + let wdrivers: str = strings.concat(td, "/ww driver copy"); + let cwd: str = strings.concat(td, "/isolated cwd"); + let work: str = strings.concat(td, "/persistent work"); + let bin: str = strings.concat(td, "/published binary"); + mkdir(source); mkdir(app); mkdir(dep); mkdir(leaf); mkdir(tools); + mkdir(cdrivers); mkdir(wdrivers); mkdir(cwd); mkdir(work); + testenv.writefile(strings.concat(leaf, "/leaf.ww"), strings.concat( + "package leaf;\n", + "export fn value() i32 = { return 40; };\n")); + testenv.writefile(strings.concat(dep, "/dep.ww"), strings.concat( + "package dep;\nimport leaf;\n", + "export fn value() i32 = { return leaf.value() + 2; };\n")); + testenv.writefile(strings.concat(app, "/main.ww"), strings.concat( + "package main;\nimport dep;\n", + "fn main() i32 = { return dep.value(); };\n")); + + let compiler: str = strings.concat(tools, "/w6c wrapper.sh"); + let assembler: str = strings.concat(tools, "/w6a wrapper.sh"); + let linker: str = strings.concat(tools, "/w6l wrapper.sh"); + testenv.writeexecutable(compiler, strings.concat( + "#!/bin/sh\n", + "printf 'BEGIN' >> \"$WW_ROOT_COMPILER_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_ROOT_COMPILER_TRACE\"; done\n", + "printf '\\n' >> \"$WW_ROOT_COMPILER_TRACE\"\n", + "exec \"$WW_ROOT_REAL_COMPILER\" \"$@\"\n")); + testenv.writeexecutable(assembler, strings.concat( + "#!/bin/sh\n", + "printf 'BEGIN' >> \"$WW_ROOT_ASSEMBLER_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_ROOT_ASSEMBLER_TRACE\"; done\n", + "printf '\\n' >> \"$WW_ROOT_ASSEMBLER_TRACE\"\n", + "exec \"$WW_ROOT_REAL_ASSEMBLER\" \"$@\"\n")); + testenv.writeexecutable(linker, strings.concat( + "#!/bin/sh\n", + "printf 'BEGIN' >> \"$WW_ROOT_LINKER_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_ROOT_LINKER_TRACE\"; done\n", + "printf '\\n' >> \"$WW_ROOT_LINKER_TRACE\"\n", + "exec \"$WW_ROOT_REAL_LINKER\" \"$@\"\n")); + + let stages: []str = ["ww", "ww_ww"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let copied: []str = [strings.concat(cdrivers, "/ww"), + strings.concat(wdrivers, "/ww_ww")]; + let runtime: str = strings.concat(testenv.repo(), "/out/lib"); + let referenceunit: str = ""; + let referencewwi: str = ""; + let referencearchive: str = ""; + let referencebin: str = ""; + let referencecompiler: str = ""; + let referenceassembler: str = ""; + let referencelinker: str = ""; + let referenceerror: str = ""; + let si: i32 = 0; + for (si < stages.len) { + let compilertrace: str = strings.concat(td, "/", tags[si], + " compiler trace"); + let assemblertrace: str = strings.concat(td, "/", tags[si], + " assembler trace"); + let linkertrace: str = strings.concat(td, "/", tags[si], + " linker trace"); + testenv.writefile(compilertrace, ""); + testenv.writefile(assemblertrace, ""); + testenv.writefile(linkertrace, ""); + let driverbytes: str = testenv.readfile(testenv.driver(stages[si])); + testenv.writeexecutable(copied[si], driverbytes); + let env: []str = roottraceenv(source, runtime, + compiler, assembler, linker, + compilertrace, assemblertrace, linkertrace, + testenv.driver(compilers[si]), + testenv.driver(assemblers[si]), + testenv.driver(linkers[si])); + let av: []str = [copied[si], "build", "-w", work, + "-o", bin, "app"]; + let out: testenv.commandout; + testenv.runcommandenv(cwd, td, strings.concat("driver-cold-", + tags[si]), av, env, tmo(), &out); + if (out.termination != exec.termination.EXIT || out.code != 0 + || out.stderr.len != 0) { + fail("driver-identity", strings.concat(tags[si], + " cold build failed: ", out.stderr)); + }; + + let rootunitpath: str = strings.concat(work, "/__root.unit.ww"); + let depunitpath: str = strings.concat(work, "/dep.unit.ww"); + let rootunit: str = testenv.readfile(rootunitpath); + let depunit: str = testenv.readfile(depunitpath); + if (!testenv.has(rootunit, "//ww:module dep\n") + || testenv.has(rootunit, "//ww:module leaf\n") + || !testenv.has(depunit, "//ww:module leaf\n")) { + fail("driver-identity", "warm units crossed the direct-export boundary"); + }; + if (!testenv.exists(strings.concat(work, "/leaf.wwi")) + || !testenv.exists(strings.concat(work, "/leaf.a")) + || !testenv.exists(strings.concat(work, "/dep.wwi")) + || !testenv.exists(strings.concat(work, "/dep.a")) + || testenv.exists(strings.concat(work, "/__root.wwi")) + || testenv.exists(strings.concat(work, "/__root.a")) + || !testenv.same(testenv.readfile(strings.concat(work, + "/.wwtool.ww")), testenv.readfile(copied[si])) + || !testenv.same(testenv.readfile(strings.concat(work, + "/.wwtool.w6c")), testenv.readfile(compiler)) + || !testenv.same(testenv.readfile(strings.concat(work, + "/.wwtool.w6a")), testenv.readfile(assembler)) + || !testenv.same(testenv.readfile(strings.concat(work, + "/.wwtool.stamp")), + "ww workdir fmt 5 mode build asm 0\n")) { + fail("driver-identity", "persistent artifacts or identities are incomplete"); + }; + let coldwwi: str = testenv.readfile(strings.concat(work, "/dep.wwi")); + let coldarchive: str = testenv.readfile(strings.concat(work, "/dep.a")); + let coldbin: str = testenv.readfile(bin); + let coldcompiler: str = testenv.readfile(compilertrace); + let coldassembler: str = testenv.readfile(assemblertrace); + let coldlinker: str = testenv.readfile(linkertrace); + if (testenv.occurrences(coldcompiler, "\n") != 3 + || !testenv.has(coldcompiler, strings.concat( + "BEGIN<-c><-I><", work, "/dep.wwi.new><-o><", work, + "/dep.s.new><", work, "/dep.unit.new>")) + || !testenv.has(coldcompiler, strings.concat( + "BEGIN<-c><-o><", work, "/__root.s.new><", work, + "/__root.unit.new>")) + || testenv.occurrences(coldassembler, "\n") != 3 + || !testenv.has(coldassembler, strings.concat( + "BEGIN<-o><", work, "/dep.o.new><", work, + "/dep.s.new>"))) { + fail("driver-identity", "cold compiler or assembler argv changed"); + }; + if (testenv.occurrences(coldlinker, "\n") != 1 + || !testenv.has(coldlinker, strings.concat("BEGIN<-o><", bin, + "><", work, "/__root.o>")) + || testenv.pos(coldlinker, strings.concat("<", work, + "/dep.a>")) < 0 + || testenv.pos(coldlinker, strings.concat("<", work, + "/leaf.a>")) < testenv.pos(coldlinker, strings.concat("<", + work, "/dep.a>")) + || !testenv.has(coldlinker, strings.concat("<", runtime, + "/libwwrt.a>")) + || testenv.has(coldlinker, ".wwi>")) { + fail("driver-identity", "cold link omitted the reachable archive closure"); + }; + let runav: []str = [bin]; + if (code(cwd, strings.concat("driver-cold-run-", tags[si]), runav) + != 42) { + fail("driver-identity", "cold binary returned the wrong value"); + }; + + testenv.runcommandenv(cwd, td, strings.concat("driver-warm-", + tags[si]), av, env, tmo(), &out); + if (out.termination != exec.termination.EXIT || out.code != 0 + || out.stderr.len != 0 + || !testenv.same(coldcompiler, testenv.readfile(compilertrace)) + || !testenv.same(coldassembler, testenv.readfile(assemblertrace)) + || testenv.occurrences(testenv.readfile(linkertrace), "\n") != 2 + || !testenv.same(rootunit, testenv.readfile(rootunitpath)) + || !testenv.same(coldwwi, testenv.readfile(strings.concat(work, + "/dep.wwi"))) + || !testenv.same(coldarchive, testenv.readfile(strings.concat(work, + "/dep.a"))) + || !testenv.same(coldbin, testenv.readfile(bin))) { + fail("driver-identity", "unchanged warm build did not reuse packages"); + }; + + assert(os.remove(copied[si]) == 0); + testenv.writeexecutable(copied[si], strings.concat(driverbytes, + "\nww driver identity mutation\n")); + testenv.runcommandenv(cwd, td, strings.concat("driver-changed-", + tags[si]), av, env, tmo(), &out); + if (out.termination != exec.termination.EXIT || out.code != 0 + || out.stderr.len != 0) { + fail("driver-identity", strings.concat(tags[si], + " changed-driver build failed: ", out.stderr)); + }; + let changedcompiler: str = testenv.readfile(compilertrace); + let changedassembler: str = testenv.readfile(assemblertrace); + let changedlinker: str = testenv.readfile(linkertrace); + if (testenv.occurrences(changedcompiler, strings.concat("<", work, + "/leaf.unit.new>")) != 2 + || testenv.occurrences(changedcompiler, strings.concat("<", work, + "/dep.unit.new>")) != 2 + || testenv.occurrences(changedcompiler, strings.concat("<", work, + "/__root.unit.new>")) != 2 + || testenv.occurrences(changedcompiler, "\n") != 6 + || testenv.occurrences(changedassembler, "\n") != 6 + || testenv.occurrences(changedlinker, "\n") != 3) { + fail("driver-identity", "driver change reused stale package actions"); + }; + if (!testenv.same(testenv.readfile(strings.concat(work, + "/.wwtool.ww")), testenv.readfile(copied[si])) + || !testenv.same(rootunit, testenv.readfile(rootunitpath)) + || !testenv.same(coldwwi, testenv.readfile(strings.concat(work, + "/dep.wwi"))) + || !testenv.same(coldarchive, testenv.readfile(strings.concat(work, + "/dep.a"))) + || !testenv.same(coldbin, testenv.readfile(bin)) + || code(cwd, strings.concat("driver-changed-run-", tags[si]), + runav) != 42) { + fail("driver-identity", "driver invalidation changed package products"); + }; + + let beforecompiler: str = strings.dup(changedcompiler); + let beforeassembler: str = strings.dup(changedassembler); + let beforelinker: str = strings.dup(changedlinker); + let missingwork: str = strings.concat(td, "/missing workdir"); + let missingbin: str = strings.concat(td, "/missing binary"); + let badav: []str = [copied[si], "build", "-w", missingwork, + "-o", missingbin, "app"]; + testenv.runcommandenv(cwd, td, strings.concat("driver-missing-", + tags[si]), badav, env, tmo(), &out); + let wantdiag: str = strings.concat("ww: workdir ", missingwork, + " is not a directory\n"); + if (out.termination != exec.termination.EXIT || out.code != 1 + || !testenv.same(out.stderr, wantdiag) + || !testenv.same(beforecompiler, testenv.readfile(compilertrace)) + || !testenv.same(beforeassembler, testenv.readfile(assemblertrace)) + || !testenv.same(beforelinker, testenv.readfile(linkertrace)) + || testenv.exists(missingbin)) { + fail("driver-identity", "invalid workdir crossed the tool boundary"); + }; + + if (si == 0) { + referenceunit = strings.dup(rootunit); + referencewwi = strings.dup(coldwwi); + referencearchive = strings.dup(coldarchive); + referencebin = strings.dup(coldbin); + referencecompiler = strings.dup(changedcompiler); + referenceassembler = strings.dup(changedassembler); + referencelinker = strings.dup(changedlinker); + referenceerror = strings.dup(out.stderr); + } else { if (!testenv.same(referenceunit, rootunit) + || !testenv.same(referencewwi, coldwwi) + || !testenv.same(referencearchive, coldarchive) + || !testenv.same(referencebin, coldbin) + || !testenv.same(referencecompiler, changedcompiler) + || !testenv.same(referenceassembler, changedassembler) + || !testenv.same(referencelinker, changedlinker) + || !testenv.same(referenceerror, out.stderr)) { + fail("driver-identity", "Cstage and WWstage warm behavior differs"); + }; }; + testenv.clean(work); + assert(os.remove(bin) == 0); + if (si + 1 < stages.len) { mkdir(work); }; + si += 1; + }; + testenv.clean(td); +}; + @test fn builtin_type_precedence() void = { let td: str = testenv.fresh(); let dep: str = strings.concat(td, "/builtinshadow"); mkdir(dep);