Skip to content

Commit 8472a93

Browse files
committed
web: navigate from unresolved to resolved CA build steps
Resolved CA steps do not own the log users are trying to inspect, so leaving them as terminal pages makes the build view look stuck or failed in the wrong place. The log page now follows the recorded resolution chain to the step that actually performed the work, while preserving a pending page for unresolved, running, or malformed chains. The redirect banner is reconstructed from BuildSteps instead of session flash so copied log URLs and refreshes keep the same context. The JSON summary exposes the same durable resolution data for API clients that need to follow CA build steps without scraping HTML.
1 parent 9c67fd6 commit 8472a93

7 files changed

Lines changed: 553 additions & 0 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
use strict;
2+
use warnings;
3+
use Setup;
4+
use JSON::MaybeXS qw(decode_json);
5+
use Nix::Config;
6+
use Test2::V0;
7+
use HTTP::Request::Common;
8+
9+
my %ctx = test_init();
10+
setup_catalyst_test($ctx{context});
11+
12+
my $db = $ctx{context}->db();
13+
14+
my $project = $db->resultset('Projects')->create({
15+
name => "tests", displayname => "", owner => "root",
16+
});
17+
my $jobset = createBaseJobset($db, "basic", "basic.nix", $ctx{jobsdir});
18+
ok(evalSucceeds($ctx{context}, $jobset), "Evaluating basic.nix succeeds");
19+
my @builds = queuedBuildsForJobset($jobset);
20+
my ($build) = grep { $_->nixname eq "empty-dir" } @builds;
21+
ok(defined $build, "got a build out of the jobset");
22+
23+
my $resolvedBasename = "00000000000000000000000000000001-resolved.drv";
24+
my $storeDir = $Nix::Config::storeDir;
25+
my $unresolvedDrv = "$storeDir/00000000000000000000000000000000-unresolved.drv";
26+
27+
$db->resultset('BuildSteps')->create({
28+
build => $build->id,
29+
stepnr => 101,
30+
type => 0,
31+
drvpath => $unresolvedDrv,
32+
busy => 0,
33+
status => 13,
34+
machine => "",
35+
resolveddrvpath => $resolvedBasename,
36+
});
37+
38+
$db->resultset('BuildSteps')->create({
39+
build => $build->id,
40+
stepnr => 102,
41+
type => 0,
42+
drvpath => "$storeDir/$resolvedBasename",
43+
busy => 0,
44+
status => 0,
45+
stoptime => 1000,
46+
machine => "",
47+
});
48+
49+
$db->resultset('BuildSteps')->create({
50+
build => $build->id,
51+
stepnr => 103,
52+
type => 0,
53+
drvpath => "$storeDir/00000000000000000000000000000002-unresolved2.drv",
54+
busy => 0,
55+
status => 13,
56+
machine => "",
57+
resolveddrvpath => "00000000000000000000000000000099-missing.drv",
58+
});
59+
60+
my $buildUrl = "/build/" . $build->id;
61+
62+
subtest "nixlog for a Resolved step 302s to the terminal" => sub {
63+
my $r = request(GET "$buildUrl/nixlog/101");
64+
is($r->code, 302, "GET nixlog of Resolved step redirects");
65+
my $loc = $r->header('location') // "";
66+
like($loc, qr{/build/\Q@{[$build->id]}\E/nixlog/102}, "Location points at terminal step");
67+
};
68+
69+
subtest "nixlog for a Resolved step whose terminal is missing renders pending template" => sub {
70+
my $r = request(GET "$buildUrl/nixlog/103");
71+
is($r->code, 200, "pending case returns 200, not a redirect");
72+
like($r->content, qr/resolved, log pending/i, "body mentions pending");
73+
like($r->content, qr/missing\.drv/, "body names the missing resolved drv");
74+
like($r->content, qr/has been scheduled yet|will refresh automatically/,
75+
"copy describes the unscheduled state, not a running one");
76+
};
77+
78+
subtest "get-info JSON exposes resolvedSteps" => sub {
79+
my $r = request(GET "$buildUrl/api/get-info", Accept => 'application/json');
80+
is($r->code, 200, "api/get-info responds");
81+
my $data = decode_json($r->content);
82+
ok($data->{resolvedSteps}, "resolvedSteps key present");
83+
is(scalar @{$data->{resolvedSteps}}, 2, "two resolved steps reported");
84+
my ($terminalEntry) = grep { $_->{stepnr} == 101 } @{$data->{resolvedSteps}};
85+
is($terminalEntry->{resolvedDrvPath}, $resolvedBasename, "step 101 reports resolved basename");
86+
is($terminalEntry->{terminal}{stepnr}, 102, "step 101 terminal is step 102");
87+
is($terminalEntry->{terminal}{status}, 0, "terminal status is Success");
88+
};
89+
90+
subtest "cycle guard: self-referencing resolved step does not loop" => sub {
91+
my $cycleBasename = "00000000000000000000000000000003-cycle.drv";
92+
$db->resultset('BuildSteps')->create({
93+
build => $build->id,
94+
stepnr => 104,
95+
type => 0,
96+
drvpath => "$storeDir/$cycleBasename",
97+
busy => 0,
98+
status => 13,
99+
machine => "",
100+
resolveddrvpath => $cycleBasename,
101+
});
102+
my $r = request(GET "$buildUrl/nixlog/104");
103+
is($r->code, 200, "self-cycle does not infinite-loop");
104+
like($r->content, qr/self-referential/i,
105+
"self-cycle copy names the failure mode, not 'not yet scheduled'");
106+
};
107+
108+
subtest "terminal log URL is shareable (banner rendered from DB, not flash)" => sub {
109+
my $r = request(GET "$buildUrl/nixlog/102");
110+
is($r->code, 200, "terminal log renders");
111+
like($r->content, qr/Redirected here from/,
112+
"banner present without a preceding redirect in the session");
113+
like($r->content, qr/step #101/, "banner names the resolved origin");
114+
like($r->content, qr/Original derivation.*unresolved\.drv/s,
115+
"banner shows the original (pre-resolution) drvpath");
116+
};
117+
118+
subtest "pending page with running terminal says 'currently running'" => sub {
119+
$db->resultset('BuildSteps')->create({
120+
build => $build->id,
121+
stepnr => 200,
122+
type => 0,
123+
drvpath => "$storeDir/00000000000000000000000000000020-pending-orig.drv",
124+
busy => 0,
125+
status => 13,
126+
machine => "",
127+
resolveddrvpath => "00000000000000000000000000000021-busy-terminal.drv",
128+
});
129+
$db->resultset('BuildSteps')->create({
130+
build => $build->id,
131+
stepnr => 201,
132+
type => 0,
133+
drvpath => "$storeDir/00000000000000000000000000000021-busy-terminal.drv",
134+
busy => 1,
135+
status => undef,
136+
machine => "builder-a",
137+
starttime => 500,
138+
});
139+
my $r = request(GET "$buildUrl/nixlog/200");
140+
is($r->code, 200, "busy-terminal case renders pending, not a redirect");
141+
like($r->content, qr/currently running/i,
142+
"pending copy distinguishes 'running' from 'unscheduled'");
143+
like($r->content, qr/step #201/, "names the running step");
144+
like($r->content, qr/http-equiv="refresh"/,
145+
"auto-refresh enabled so page updates when terminal finishes");
146+
};
147+
148+
subtest "pending page with unscheduled terminal does not auto-refresh self-cycle" => sub {
149+
my $r = request(GET "$buildUrl/nixlog/104");
150+
unlike($r->content, qr/http-equiv="refresh"/,
151+
"self-cycle never resolves, so no meta-refresh spin");
152+
};
153+
154+
subtest "cross-build scoping: chain does not reach into other builds" => sub {
155+
my $buildB = $db->resultset('Builds')->create({
156+
finished => 1,
157+
timestamp => 0,
158+
jobset_id => $build->jobset_id,
159+
job => "empty-dir",
160+
nixname => "empty-dir",
161+
drvpath => "$storeDir/00000000000000000000000000000040-other-unresolved.drv",
162+
system => "x86_64-linux",
163+
starttime => 1, stoptime => 1,
164+
buildstatus => 0, iscurrent => 0,
165+
});
166+
167+
$db->resultset('BuildSteps')->create({
168+
build => $build->id,
169+
stepnr => 301,
170+
type => 0,
171+
drvpath => "$storeDir/00000000000000000000000000000040-origA.drv",
172+
busy => 0,
173+
status => 13,
174+
machine => "",
175+
resolveddrvpath => "00000000000000000000000000000041-collide.drv",
176+
});
177+
178+
$db->resultset('BuildSteps')->create({
179+
build => $buildB->id,
180+
stepnr => 500,
181+
type => 0,
182+
drvpath => "$storeDir/00000000000000000000000000000041-collide.drv",
183+
busy => 0,
184+
status => 0,
185+
stoptime => 1000,
186+
machine => "",
187+
});
188+
189+
my $r = request(GET "$buildUrl/nixlog/301");
190+
is($r->code, 200, "no cross-build terminal -> pending, not redirect");
191+
like($r->content, qr/resolved, log pending/i,
192+
"chain stops at build boundary, renders pending template");
193+
};
194+
195+
subtest "cycle detection: two siblings sharing resolveddrvpath don't false-cycle" => sub {
196+
$db->resultset('BuildSteps')->create({
197+
build => $build->id,
198+
stepnr => 400,
199+
type => 0,
200+
drvpath => "$storeDir/00000000000000000000000000000050-first.drv",
201+
busy => 0,
202+
status => 13,
203+
machine => "",
204+
resolveddrvpath => "00000000000000000000000000000051-shared.drv",
205+
});
206+
$db->resultset('BuildSteps')->create({
207+
build => $build->id,
208+
stepnr => 401,
209+
type => 0,
210+
drvpath => "$storeDir/00000000000000000000000000000060-second.drv",
211+
busy => 0,
212+
status => 13,
213+
machine => "",
214+
resolveddrvpath => "00000000000000000000000000000051-shared.drv",
215+
});
216+
$db->resultset('BuildSteps')->create({
217+
build => $build->id,
218+
stepnr => 402,
219+
type => 0,
220+
drvpath => "$storeDir/00000000000000000000000000000051-shared.drv",
221+
busy => 0,
222+
status => 0,
223+
stoptime => 1000,
224+
machine => "",
225+
});
226+
227+
for my $origin (400, 401) {
228+
my $r = request(GET "$buildUrl/nixlog/$origin");
229+
is($r->code, 302, "step $origin redirects (no false cycle)");
230+
like($r->header('location') // "", qr{/nixlog/402},
231+
"step $origin lands on the shared terminal");
232+
}
233+
};
234+
235+
subtest "basename validation: resolveddrvpath with a slash is rejected" => sub {
236+
$db->resultset('BuildSteps')->create({
237+
build => $build->id,
238+
stepnr => 500,
239+
type => 0,
240+
drvpath => "$storeDir/00000000000000000000000000000070-corrupt.drv",
241+
busy => 0,
242+
status => 13,
243+
machine => "",
244+
resolveddrvpath => "../../etc/passwd",
245+
});
246+
my $r = request(GET "$buildUrl/nixlog/500");
247+
is($r->code, 200, "corrupt basename does not crash or chase");
248+
like($r->content, qr/resolved, log pending/i,
249+
"falls through to pending page rather than following the hop");
250+
};
251+
252+
subtest "multi-step resolution cycle is classified as cycle, not unscheduled" => sub {
253+
$db->resultset('BuildSteps')->create({
254+
build => $build->id,
255+
stepnr => 600,
256+
type => 0,
257+
drvpath => "$storeDir/00000000000000000000000000000080-cycle-a.drv",
258+
busy => 0,
259+
status => 13,
260+
machine => "",
261+
resolveddrvpath => "00000000000000000000000000000081-cycle-b.drv",
262+
});
263+
$db->resultset('BuildSteps')->create({
264+
build => $build->id,
265+
stepnr => 601,
266+
type => 0,
267+
drvpath => "$storeDir/00000000000000000000000000000081-cycle-b.drv",
268+
busy => 0,
269+
status => 13,
270+
machine => "",
271+
resolveddrvpath => "00000000000000000000000000000080-cycle-a.drv",
272+
});
273+
my $r = request(GET "$buildUrl/nixlog/600");
274+
is($r->code, 200, "multi-step cycle renders pending, not redirect");
275+
like($r->content, qr/self-referential|never produce/i,
276+
"multi-step cycle uses the cycle copy, not 'unscheduled'");
277+
unlike($r->content, qr/http-equiv="refresh"/,
278+
"multi-step cycle does not auto-refresh");
279+
};
280+
281+
subtest "raw mode on a Resolved step does not set a lingering flash banner" => sub {
282+
my $r1 = request(GET "$buildUrl/nixlog/101/raw");
283+
is($r1->code, 302, "raw on a resolved step still redirects");
284+
my $r2 = request(GET "/");
285+
unlike($r2->content, qr/Redirected here from/,
286+
"no lingering flash banner on unrelated page");
287+
};
288+
289+
done_testing;

0 commit comments

Comments
 (0)