mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
test(browser): the exit code a dead run and a violated one actually leave
Drives the built binary against a page with nothing to tap and reads the process status, then the same run through campaign to pin what lands in runs.jsonl: exit_code 1 there is a detection the analysis drops as missing data.
This commit is contained in:
1 parent
f4b9ff1468
commit
c8d52cb7d5
4 files changed
+208
No files matched your search
@@ -0,0 +1,180 @@
|
||||
//go:build browser
|
||||
|
||||
package browser_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The nothing-to-tap fixture is a page holding one line of static text, so the
|
||||
// seeded generator finds no candidate on any step and the run ends having
|
||||
// driven the app zero times. It is the shape a provider outage gives a model
|
||||
// arm, reproduced without a provider.
|
||||
const deadRunFixture = "nothing-to-tap"
|
||||
|
||||
// TestBrowserDeadRunExitCodes drives the real binary against that page and
|
||||
// reads the status the shell would: the refusal is only worth what the process
|
||||
// exit code says, and CI, campaign and the analysis all read it rather than the
|
||||
// error value.
|
||||
func TestBrowserDeadRunExitCodes(t *testing.T) {
|
||||
binary := buildBinary(t, "./cmd/sanderling")
|
||||
pageURL := servePage(t)
|
||||
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
spec string
|
||||
flags []string
|
||||
wantExit int
|
||||
wantSays string
|
||||
}{
|
||||
{
|
||||
name: "a run that explored nothing is refused",
|
||||
spec: "spec.ts",
|
||||
wantExit: 1,
|
||||
wantSays: "the action generator drove the app in none of them",
|
||||
},
|
||||
{
|
||||
name: "the property-free flag waives the refusal it names and no other",
|
||||
spec: "spec.ts",
|
||||
flags: []string{"--allow-no-properties"},
|
||||
wantExit: 1,
|
||||
wantSays: "the action generator drove the app in none of them",
|
||||
},
|
||||
{
|
||||
name: "the dead-run flag waives the dead-run refusal",
|
||||
spec: "spec.ts",
|
||||
flags: []string{"--allow-no-generator-actions"},
|
||||
wantExit: 0,
|
||||
},
|
||||
{
|
||||
// The campaign case: no flags are passed there, and a run that
|
||||
// recorded a violation has produced the verdict the refusal exists
|
||||
// to demand.
|
||||
name: "a recorded violation carries the run without any flag",
|
||||
spec: "violated-spec.ts",
|
||||
wantExit: 0,
|
||||
},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
arguments := append(runFlags(t, testCase.spec, pageURL), testCase.flags...)
|
||||
exitCode, output := runBinary(t, binary, arguments)
|
||||
|
||||
if exitCode != testCase.wantExit {
|
||||
t.Fatalf("exit code: got %d, want %d\n%s", exitCode, testCase.wantExit, output)
|
||||
}
|
||||
if testCase.wantSays != "" && !strings.Contains(output, testCase.wantSays) {
|
||||
t.Errorf("the run never said %q:\n%s", testCase.wantSays, output)
|
||||
}
|
||||
if testCase.wantExit != 0 && !strings.Contains(output, "--allow-no-generator-actions") {
|
||||
t.Errorf("the refusal never names the flag that waives it:\n%s", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBrowserCampaignRecordsAViolationFoundWithoutTheGenerator follows the
|
||||
// whole chain a campaign is read through. A campaign passes no
|
||||
// --exit-on-violation, so a refusal here writes exit_code 1 and the analysis
|
||||
// drops the run as missing data: the detection the campaign exists to count
|
||||
// disappears into the excluded pile.
|
||||
func TestBrowserCampaignRecordsAViolationFoundWithoutTheGenerator(t *testing.T) {
|
||||
sanderling := buildBinary(t, "./cmd/sanderling")
|
||||
campaign := buildBinary(t, "./cmd/internal-tools/campaign")
|
||||
pageURL := servePage(t)
|
||||
campaignDirectory := filepath.Join(t.TempDir(), "campaign")
|
||||
|
||||
exitCode, output := runBinary(t, campaign, []string{
|
||||
"--spec", fixtureSpec(t, "violated-spec.ts"),
|
||||
"--bundle-id", pageURL,
|
||||
"--platform", "web",
|
||||
"--arm", "dead-run",
|
||||
"--generator", "seeded",
|
||||
"--max-steps", "3",
|
||||
"--duration", "60s",
|
||||
"--seeds", "1",
|
||||
"--sanderling", sanderling,
|
||||
"--output", campaignDirectory,
|
||||
})
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("campaign exit code: got %d, want 0\n%s", exitCode, output)
|
||||
}
|
||||
|
||||
body, err := os.ReadFile(filepath.Join(campaignDirectory, "runs.jsonl"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var record struct {
|
||||
ExitCode int `json:"exit_code"`
|
||||
Actions int `json:"actions"`
|
||||
ViolatedProperties []string `json:"violated_properties"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &record); err != nil {
|
||||
t.Fatalf("runs.jsonl: %v\n%s", err, body)
|
||||
}
|
||||
|
||||
if record.ExitCode != 0 {
|
||||
t.Errorf("exit_code: got %d, want 0. The analysis excludes a non-zero record as "+
|
||||
"missing data, so this violation would never reach the survival analysis", record.ExitCode)
|
||||
}
|
||||
if record.Actions != 0 {
|
||||
t.Fatalf("actions: got %d, want 0: the fixture offers the generator nothing to drive", record.Actions)
|
||||
}
|
||||
if len(record.ViolatedProperties) != 1 || record.ViolatedProperties[0] != "bannerIsEmpty" {
|
||||
t.Errorf("violated_properties: got %v, want [bannerIsEmpty]", record.ViolatedProperties)
|
||||
}
|
||||
}
|
||||
|
||||
func runFlags(t *testing.T, spec, pageURL string) []string {
|
||||
t.Helper()
|
||||
return []string{
|
||||
"test",
|
||||
"--platform", "web",
|
||||
"--spec", fixtureSpec(t, spec),
|
||||
"--bundle-id", pageURL,
|
||||
"--duration", "60s",
|
||||
"--max-steps", "3",
|
||||
"--seed", "1",
|
||||
"--output", t.TempDir(),
|
||||
}
|
||||
}
|
||||
|
||||
func fixtureSpec(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
return filepath.Join(testdataDir(t), deadRunFixture, name)
|
||||
}
|
||||
|
||||
func servePage(t *testing.T) string {
|
||||
t.Helper()
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(testdataDir(t))))
|
||||
t.Cleanup(server.Close)
|
||||
return server.URL + "/" + deadRunFixture + "/"
|
||||
}
|
||||
|
||||
func buildBinary(t *testing.T, packagePath string) string {
|
||||
t.Helper()
|
||||
binary := filepath.Join(t.TempDir(), filepath.Base(packagePath))
|
||||
build := exec.Command("go", "build", "-o", binary, packagePath)
|
||||
build.Dir = repoRoot(t)
|
||||
if output, err := build.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build %s: %v\n%s", packagePath, err, output)
|
||||
}
|
||||
return binary
|
||||
}
|
||||
|
||||
func runBinary(t *testing.T, binary string, arguments []string) (int, string) {
|
||||
t.Helper()
|
||||
command := exec.Command(binary, arguments...)
|
||||
command.Dir = repoRoot(t)
|
||||
output, err := command.CombinedOutput()
|
||||
if command.ProcessState == nil {
|
||||
t.Fatalf("%s never ran: %v", binary, err)
|
||||
}
|
||||
return command.ProcessState.ExitCode(), string(output)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>nothing-to-tap</title>
|
||||
</head>
|
||||
<body>
|
||||
<p id="banner">nothing here answers a tap</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { always, extract, taps } from "@sanderling/spec";
|
||||
|
||||
const banner = extract((s) => s.ax.find({ id: "banner" })?.text ?? "").named("banner");
|
||||
|
||||
const bannerStays = always(() => banner.current === "nothing here answers a tap");
|
||||
|
||||
export const properties = { bannerStays };
|
||||
|
||||
export const actionsRoot = taps;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { always, extract, taps } from "@sanderling/spec";
|
||||
|
||||
const banner = extract((s) => s.ax.find({ id: "banner" })?.text ?? "").named("banner");
|
||||
|
||||
const bannerIsEmpty = always(() => banner.current === "");
|
||||
|
||||
export const properties = { bannerIsEmpty };
|
||||
|
||||
export const actionsRoot = taps;
|
||||
Reference in new issue
Block a user