Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions e2e/appduct-fixtures.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { describe, expect, it } from "vitest";

const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const FIXTURES = "e2e/fixtures/appduct";
const README = `${FIXTURES}/README.md`;

interface Tracked {
/** why: path relative to `e2e/fixtures/appduct/`. */
path: string;
mode: string;
}

function trackedFixtures(): Tracked[] {
return execFileSync("git", ["ls-files", "-s", "-z", FIXTURES], {
cwd: REPO_ROOT,
encoding: "buffer",
})
.toString("utf8")
.split("\0")
.filter((entry) => entry.length > 0)
.map((entry) => {
const [meta = "", path = ""] = entry.split("\t");
return { mode: meta.split(" ")[0] ?? "", path: path.slice(FIXTURES.length + 1) };
});
}

function readme(): string {
return readFileSync(join(REPO_ROOT, README), "utf8");
}

/** why: the rows of the only table under `## Files`: path and SHA-256, cells trimmed, backticks removed. */
function readmeTable(): Array<{ path: string; sha256: string }> {
const section = readme().split(/^## Files\s*$/m)[1] ?? "";
const body = section.split(/^## /m)[0] ?? "";
return body
.split("\n")
.filter((line) => line.trim().startsWith("|"))
.map((line) =>
line
.split("|")
.slice(1, -1)
.map((cell) => cell.trim()),
)
.filter((cells) => cells.length === 2 && !/^-+$/.test(cells[0] ?? "") && cells[0] !== "File")
.map(([path = "", sha256 = ""]) => ({
path: path.replace(/^`|`$/g, ""),
sha256: sha256.replace(/^`|`$/g, ""),
}));
}

describe("Appduct fixture apps", () => {
it("lists every tracked file under e2e/fixtures/appduct/ except the README in the README's table, with the SHA-256 of the file on disk", () => {
const files = trackedFixtures()
.map((file) => file.path)
.filter((path) => path !== "README.md");
const table = readmeTable();

expect(files.length).toBeGreaterThan(0);
expect(table.map((row) => row.path).sort()).toEqual(expect.arrayContaining(files.sort()));
for (const row of table.filter((entry) => files.includes(entry.path))) {
expect(row.sha256).toMatch(/^[0-9a-f]{64}$/);
const onDisk = createHash("sha256")
.update(readFileSync(join(REPO_ROOT, FIXTURES, row.path)))
.digest("hex");
expect(onDisk, row.path).toBe(row.sha256);
}
});

it("tracks ios/AppductPlayground.app/Info.plist, ios/AppductPlayground.app/AppductPlayground and android/AppductPlayground.apk under exactly those names", () => {
const paths = trackedFixtures().map((file) => file.path);

expect(paths).toEqual(
expect.arrayContaining([
"ios/AppductPlayground.app/Info.plist",
"ios/AppductPlayground.app/AppductPlayground",
"android/AppductPlayground.apk",
]),
);
});

it("lists no file in the README's table that git does not track under e2e/fixtures/appduct/", () => {
const tracked = trackedFixtures().map((file) => file.path);
const table = readmeTable();

expect(table.length).toBeGreaterThan(0);
expect(table.map((row) => row.path).filter((path) => !tracked.includes(path))).toEqual([]);
});

it("tracks the iOS executable ios/AppductPlayground.app/AppductPlayground with mode 100755", () => {
const executable = trackedFixtures().find(
(file) => file.path === "ios/AppductPlayground.app/AppductPlayground",
);

expect(executable?.mode).toBe("100755");
});

it("has a Repo: line naming https://github.com/callstackincubator/appduct and a Commit: line with 40 lowercase hex digits", () => {
const text = readme();

expect(text).toMatch(/^Repo: https:\/\/github\.com\/callstackincubator\/appduct$/m);
expect(text).toMatch(/^Commit: `[0-9a-f]{40}`$/m);
});
});
75 changes: 75 additions & 0 deletions e2e/fixtures/appduct/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Appduct playground fixture apps

Two built apps for the agent-device slow tests (#477): an iOS simulator build
and an Android debug build of Appduct's native playground. They are committed
so a test installs them without building an app or downloading anything.

Repo: https://github.com/callstackincubator/appduct
Commit: `0e68131bdd4b8a789314812caf1995b496c52ae9`

The commit is the tag `v0.15.0`. Both apps are built from it.

## Build commands

```bash
# iOS, from playground-native/ios at the pin (needs Xcode and xcodegen)
xcodegen generate
xcodebuild build -project AppductPlayground.xcodeproj -scheme AppductPlayground \
-configuration Debug -sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' -derivedDataPath build \
ARCHS=arm64 CODE_SIGNING_ALLOWED=NO
# -> build/Build/Products/Debug-iphonesimulator/AppductPlayground.app

# Android, from playground-native/android at the pin (needs JDK 17 and ANDROID_HOME)
./gradlew :app:assembleDebug
# -> app/build/outputs/apk/debug/app-debug.apk, committed as AppductPlayground.apk
```

Where each output goes:

- `AppductPlayground.app` is copied whole to `ios/AppductPlayground.app/`.
- `app-debug.apk` is copied to `android/AppductPlayground.apk`.

## Tool versions of this build

- Xcode 27.0 (build 27A266a)
- xcodegen 2.45.4
- JDK 17 (Zulu), Gradle 8.14.3 (the wrapper's)
- Android SDK platform 35 (`compileSdk`)

## App ids

- iOS: `com.callstack.appduct.playgroundnative`
- Android: `com.callstack.appduct.playground`

## Screen ids

From Appduct's playground contract; #477 uses these: `tab-tools`,
`tab-status`, `connection-state`, `call-count`.

## Notes

- Both are debug builds, as Appduct's own device suite uses.
- The iOS build runs on arm64 simulators only (Apple silicon Macs). It is
unsigned; the arm64 linker signs it ad hoc, which the simulator accepts.
- The Android build is signed with the builder's debug key.

## Files

| File | SHA-256 |
| --------------------------------------------------------- | ------------------------------------------------------------------ |
| `android/AppductPlayground.apk` | `023e23a7f47f53c0b7914088b6f91d802212297faad126dadf1de81982d9f2a7` |
| `ios/AppductPlayground.app/AppductPlayground` | `1d434163aab86e0266796e6fada8619c296193e086f84ddd1495290c4ba27fea` |
| `ios/AppductPlayground.app/AppductPlayground.debug.dylib` | `d4e2ef2949c1a1b27ee70bc79b23b40dd8b470bf98047372503f6c9dc1ee38ca` |
| `ios/AppductPlayground.app/Info.plist` | `444c14b8188fd231c52e4e3711ca681ec370ad1883d145e4ef6533c05e52dc90` |
| `ios/AppductPlayground.app/PkgInfo` | `82502191c9484b04d685374f9879a0066069c49b8acae7a04b01d38d07e8eca0` |
| `ios/AppductPlayground.app/__preview.dylib` | `2b569eb3932de0d2908fa99589f9078d6d9abc5bea8c9fbf03d84980986dd287` |

## Refresh

1. Build at the new pin with the commands above.
2. Replace the files under `ios/` and `android/`.
3. Update the tag, the commit, the tool versions and the table above in the
same commit.
4. Check by hand: `plutil -extract CFBundleIdentifier raw` on `Info.plist`,
`lipo -info` on the executable, and `aapt dump badging` on the APK.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions e2e/fixtures/appduct/ios/AppductPlayground.app/PkgInfo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
APPL????
Binary file not shown.
2 changes: 1 addition & 1 deletion e2e/physical-leasing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,9 +510,9 @@ describe("a physical lease request that waits", () => {
},
);
const first = await granted(env, physical("agent-a"));
await release(env, first);
const waiter = env.cliBackground(physical("agent-b"));
await queuedAt(waiter, 1);
await release(env, first);

const ended = await waiter.waitForExit(30_000);

Expand Down
13 changes: 10 additions & 3 deletions src/source-hygiene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const NUL = 0x00;

/**
* A raw NUL byte in a text file makes `grep` and `ripgrep` classify the whole file as
* why: a raw NUL byte in a text file makes `grep` and `ripgrep` classify the whole file as
* binary and print `binary file matches` instead of the matching lines -- so the file
* stops being searchable for every human and agent working in this repo. The byte is
* almost always a `"\0"` escape that was written as the literal character instead.
*
* Enumerated from `git ls-files` rather than a directory walk so the check sees every
* tracked file in the repository (testing rule 4), not just the directories this test
* happened to name. Untracked and ignored files are out of scope: they are not part of
* happened to name. The one exception is `e2e/fixtures/`: built binaries committed for tests,
* never searched as text, so they are skipped. Untracked and ignored files are out of scope: they are not part of
* what anyone clones or searches.
*/
describe("tracked files", () => {
Expand All @@ -29,6 +30,7 @@ describe("tracked files", () => {
.toString("utf8")
.split("\0")
.filter((name) => name.length > 0)
.filter((name) => !name.startsWith("e2e/fixtures/"))
// A submodule or a path deleted from the working tree is still listed by
// `git ls-files`; only regular files can be read.
.filter((name) => {
Expand All @@ -45,7 +47,12 @@ describe("tracked files", () => {
expect(trackedFiles.length).toBeGreaterThan(100);
});

it("contain no raw NUL byte, so grep and ripgrep can search them", () => {
it("hold no path under e2e/fixtures/ and still hold e2e/appduct-fixtures.test.ts", () => {
expect(trackedFiles.filter((name) => name.startsWith("e2e/fixtures/"))).toEqual([]);
expect(trackedFiles).toContain("e2e/appduct-fixtures.test.ts");
});

it("outside e2e/fixtures/ contain no raw NUL byte, so grep and ripgrep can search them", () => {
const offenders = trackedFiles
.map((name) => ({ name, contents: readFileSync(join(REPO_ROOT, name)) }))
.filter(({ contents }) => contents.includes(NUL))
Expand Down
Loading