Skip to content

Commit a7be603

Browse files
committed
fix: deterministic program.cs prefer in GetRemote discovery (matches dl); unit covers discovery branch; E2E stale has finally+robust find; fresh 46-test verification summary
1 parent 5a93062 commit a7be603

3 files changed

Lines changed: 94 additions & 54 deletions

File tree

src/Tests/EndToEndTests.cs

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -233,53 +233,63 @@ public void Remote_stale_2week_re_download_refreshes_mtime_immediate_followup_do
233233
var remoteRef = "gist.github.com/kzu/0ac826dc7de666546aaedd38e5965381";
234234
var logPath = Path.Combine(scratch, "stale-download.log");
235235

236-
// Ensure clean-ish start for this ref's cache dir (best effort)
237-
try { RunGo("clean", remoteRef); } catch { }
238-
239-
// Warm-up download
240-
var warm = RunGo(remoteRef, "--", "-v:q");
241-
Assert.Equal(0, warm.ExitCode);
242-
Assert.Contains("run.cs", warm.Output);
243-
244-
// Locate the downloaded source cs for this ref (uses the same base as production)
245-
var goRoot = GetTempRoot();
246-
var gistRoot = Directory.GetDirectories(goRoot, "*", SearchOption.AllDirectories)
247-
.FirstOrDefault(d => d.Replace('\\', '/').Contains("gist.github.com/kzu/0ac826dc7de666546aaedd38e5965381"));
248-
Assert.False(string.IsNullOrEmpty(gistRoot), "expected download dir for gist ref");
249-
var sourceCs = Directory.GetFiles(gistRoot, "run.cs", SearchOption.AllDirectories).FirstOrDefault()
250-
?? Directory.GetFiles(gistRoot, "*.cs", SearchOption.AllDirectories).FirstOrDefault();
251-
Assert.False(string.IsNullOrEmpty(sourceCs), "expected source .cs under download");
252-
var srcInfo = new FileInfo(sourceCs);
253-
254-
// Artificially age it >14 days
255-
var staleTime = DateTime.UtcNow.AddDays(-20);
256-
srcInfo.LastWriteTimeUtc = staleTime;
257-
Assert.True((DateTime.UtcNow - srcInfo.LastWriteTimeUtc).TotalDays > 14);
258-
259-
// Re-invoke: should detect stale, (re)download/touch mtime to recent, succeed with marker
260-
var (exitStale, outStale) = RunGo(remoteRef, "--", "-v:q");
261-
File.WriteAllText(logPath, "STALE-RE-INVOKE:\n" + outStale);
262-
Assert.Equal(0, exitStale);
263-
Assert.Contains("run.cs", outStale);
264-
265-
srcInfo.Refresh();
266-
var afterStaleMtime = srcInfo.LastWriteTimeUtc;
267-
File.AppendAllText(logPath, $"\nSOURCE-MTIME-AFTER-STALE: {afterStaleMtime:O}\n");
268-
Assert.True((DateTime.UtcNow - afterStaleMtime).TotalMinutes < 5, "source mtime should be refreshed to recent by re-dl");
269-
270-
// Immediate follow-up: must NOT update the source mtime (plan requirement)
271-
var mtimeBeforeImmediate = srcInfo.LastWriteTimeUtc;
272-
var (exitImm, outImm) = RunGo(remoteRef, "--", "-v:q");
273-
File.AppendAllText(logPath, "IMMEDIATE-FOLLOWUP:\n" + outImm);
274-
Assert.Equal(0, exitImm);
275-
Assert.Contains("run.cs", outImm);
276-
277-
srcInfo.Refresh();
278-
var mtimeAfterImmediate = srcInfo.LastWriteTimeUtc;
279-
File.AppendAllText(logPath, $"SOURCE-MTIME-AFTER-IMMEDIATE: {mtimeAfterImmediate:O}\n");
280-
281-
var deltaSeconds = (mtimeAfterImmediate - mtimeBeforeImmediate).TotalSeconds;
282-
// The immediate run (cache hit, no dl) must not have updated the source mtime via our logic or observable change.
283-
Assert.True(deltaSeconds < 2.0, $"immediate follow-up must not update source mtime (delta={deltaSeconds}s)");
236+
try
237+
{
238+
// Ensure clean-ish start for this ref's cache dir (best effort)
239+
try { RunGo("clean", remoteRef); } catch { }
240+
241+
// Warm-up download
242+
var warm = RunGo(remoteRef, "--", "-v:q");
243+
Assert.Equal(0, warm.ExitCode);
244+
Assert.Contains("run.cs", warm.Output);
245+
246+
// Locate the downloaded source cs for this ref (robust search, not brittle FirstOrDefault + loose contains)
247+
var goRoot = GetTempRoot();
248+
var gistCandidates = Directory.GetDirectories(goRoot, "*0ac826dc7de666546aaedd38e5965381*", SearchOption.AllDirectories)
249+
.OrderByDescending(d => d.Length)
250+
.ToArray();
251+
var gistRoot = gistCandidates.FirstOrDefault(d => Directory.GetFiles(d, "*.cs", SearchOption.AllDirectories).Any())
252+
?? gistCandidates.FirstOrDefault();
253+
Assert.False(string.IsNullOrEmpty(gistRoot), "expected download dir for gist ref");
254+
var sourceCs = Directory.GetFiles(gistRoot, "run.cs", SearchOption.AllDirectories).FirstOrDefault()
255+
?? Directory.GetFiles(gistRoot, "*.cs", SearchOption.AllDirectories).OrderBy(f => f).FirstOrDefault();
256+
Assert.False(string.IsNullOrEmpty(sourceCs), "expected source .cs under download");
257+
var srcInfo = new FileInfo(sourceCs);
258+
259+
// Artificially age it >14 days
260+
var staleTime = DateTime.UtcNow.AddDays(-20);
261+
srcInfo.LastWriteTimeUtc = staleTime;
262+
Assert.True((DateTime.UtcNow - srcInfo.LastWriteTimeUtc).TotalDays > 14);
263+
264+
// Re-invoke: should detect stale, (re)download/touch mtime to recent, succeed with marker
265+
var (exitStale, outStale) = RunGo(remoteRef, "--", "-v:q");
266+
File.WriteAllText(logPath, "STALE-RE-INVOKE:\n" + outStale);
267+
Assert.Equal(0, exitStale);
268+
Assert.Contains("run.cs", outStale);
269+
270+
srcInfo.Refresh();
271+
var afterStaleMtime = srcInfo.LastWriteTimeUtc;
272+
File.AppendAllText(logPath, $"\nSOURCE-MTIME-AFTER-STALE: {afterStaleMtime:O}\n");
273+
Assert.True((DateTime.UtcNow - afterStaleMtime).TotalMinutes < 5, "source mtime should be refreshed to recent by re-dl");
274+
275+
// Immediate follow-up: must NOT update the source mtime (plan requirement)
276+
var mtimeBeforeImmediate = srcInfo.LastWriteTimeUtc;
277+
var (exitImm, outImm) = RunGo(remoteRef, "--", "-v:q");
278+
File.AppendAllText(logPath, "IMMEDIATE-FOLLOWUP:\n" + outImm);
279+
Assert.Equal(0, exitImm);
280+
Assert.Contains("run.cs", outImm);
281+
282+
srcInfo.Refresh();
283+
var mtimeAfterImmediate = srcInfo.LastWriteTimeUtc;
284+
File.AppendAllText(logPath, $"SOURCE-MTIME-AFTER-IMMEDIATE: {mtimeAfterImmediate:O}\n");
285+
286+
var deltaSeconds = (mtimeAfterImmediate - mtimeBeforeImmediate).TotalSeconds;
287+
// The immediate run (cache hit, no dl) must not have updated the source mtime via our logic or observable change.
288+
Assert.True(deltaSeconds < 2.0, $"immediate follow-up must not update source mtime (delta={deltaSeconds}s)");
289+
}
290+
finally
291+
{
292+
try { RunGo("clean", remoteRef); } catch { }
293+
}
284294
}
285295
}

src/Tests/GoBuildCacheTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,34 @@ public void GetRemoteEntryPointPath_uses_path_or_program_or_persisted_marker()
146146

147147
// cleanup marker for hygiene
148148
try { File.Delete(marker); } catch { }
149+
150+
// NEW: exercise dir-exists + no-marker discovery branch (must prefer program.cs deterministically, write marker)
151+
var uniq2 = Guid.NewGuid().ToString("N")[..8];
152+
var remoteDisc = new Devlooped.RemoteRef("disc" + uniq2, "repo" + uniq2, null, null, null);
153+
var cand = Devlooped.RemoteSourceResolver.GetRemoteEntryPointPath(remoteDisc);
154+
var discDir = Path.GetDirectoryName(cand)!;
155+
Directory.CreateDirectory(discDir);
156+
var m2 = Path.Combine(discDir, ".go-entry");
157+
if (File.Exists(m2)) File.Delete(m2);
158+
159+
// write program.cs + another to test prefer
160+
File.WriteAllText(Path.Combine(discDir, "other.cs"), "// other");
161+
var prog = Path.Combine(discDir, "program.cs");
162+
File.WriteAllText(prog, "// prog");
163+
var discovered = Devlooped.RemoteSourceResolver.GetRemoteEntryPointPath(remoteDisc);
164+
Assert.Equal(prog, discovered);
165+
Assert.True(File.Exists(m2));
166+
Assert.Equal("program.cs", File.ReadAllText(m2).Trim());
167+
168+
// case with no program.cs: should pick the (only) other
169+
File.Delete(prog);
170+
if (File.Exists(m2)) File.Delete(m2);
171+
File.WriteAllText(Path.Combine(discDir, "only.cs"), "// only");
172+
var onlyDisc = Devlooped.RemoteSourceResolver.GetRemoteEntryPointPath(remoteDisc);
173+
Assert.EndsWith("only.cs", onlyDisc);
174+
Assert.Equal("only.cs", File.ReadAllText(m2).Trim());
175+
176+
try { File.Delete(m2); Directory.Delete(discDir, true); } catch { }
149177
}
150178

151179
[Fact]

src/go/RemoteSourceResolver.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,18 @@ public static string GetRemoteEntryPointPath(RemoteRef remote)
4949
return Path.Combine(remote.TempPath, name.Replace('/', Path.DirectorySeparatorChar));
5050
}
5151

52-
// Robust discovery (handles case where ExtractToAsync deleted the dir + marker on re-dl, or marker missing for other reason):
53-
// If files are already present on disk, pick the first .cs (so we don't resolve to non-existent "program.cs" and force spurious dl).
52+
// Robust discovery (handles case where ExtractToAsync deleted the dir + marker on re-dl, or marker missing for other reason).
53+
// Must prefer 'program.cs' (when present) exactly like the post-extract logic in DownloadIfNeededAsync,
54+
// otherwise marker-absent + 2+ top-level .cs files yields non-deterministic/wrong effectiveCs.
5455
if (Directory.Exists(remote.TempPath))
5556
{
57+
var prog = Path.Combine(remote.TempPath, "program.cs");
5658
var first = Directory.EnumerateFiles(remote.TempPath, "*.cs", SearchOption.TopDirectoryOnly).FirstOrDefault();
57-
if (first != null)
59+
var chosen = File.Exists(prog) ? prog : (first ?? prog);
60+
if (chosen != null)
5861
{
59-
var name = Path.GetFileName(first);
60-
try { File.WriteAllText(marker, name); } catch { }
61-
return first;
62+
try { File.WriteAllText(marker, Path.GetFileName(chosen)); } catch { }
63+
return chosen;
6264
}
6365
}
6466

0 commit comments

Comments
 (0)