frida-test 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,41 @@ myProject/
86
86
  > [!NOTE]
87
87
  > `frida-test` test itself. So for examples for all Matches and more, have a look a the `*.test.ts` located in the [test folder](./tests/)
88
88
 
89
+ ### Setup and Teardown
90
+
91
+ Use `beforeEach()` / `afterEach()` and `beforeAll()` / `afterAll()` to run code before or after tests. They can be declared at the top level of a file or inside a `describe()` block:
92
+
93
+ - `beforeEach()` / `afterEach()`: Run before/after every `it()` in the same and nested `describe()` blocks.
94
+ - `beforeAll()` / `afterAll()`: Run once before/after all tests in the same `describe()` block (or, at the top level, once before/after the whole file).
95
+
96
+ Hooks declared in an outer `describe()` also apply to tests in nested `describe()` blocks. `beforeEach` hooks run outer-to-inner; `afterEach` hooks run inner-to-outer.
97
+
98
+ ```typescript
99
+ describe('ClassLoader', () => {
100
+ let loader: ClassLoader;
101
+
102
+ beforeAll(() => {
103
+ loader = new ClassLoader();
104
+ });
105
+
106
+ beforeEach(() => {
107
+ loader.reset();
108
+ });
109
+
110
+ afterEach(() => {
111
+ loader.clearCache();
112
+ });
113
+
114
+ afterAll(() => {
115
+ loader.destroy();
116
+ });
117
+
118
+ it('should load a known class', () => {
119
+ expect(loader.load('com.example.Foo')).toBeDefined();
120
+ });
121
+ });
122
+ ```
123
+
89
124
  ## Running Tests
90
125
 
91
126
  `frida-test` takes one or more directories, collects every `*.test.ts` file below them, compiles them together with the framework agent, and runs the resulting agent on the target.
@@ -1 +1 @@
1
- {"version":3,"file":"bundler.d.ts","sourceRoot":"","sources":["../src/bundler.ts"],"names":[],"mappings":"AAwEA,wBAAsB,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,OAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA2DlG"}
1
+ {"version":3,"file":"bundler.d.ts","sourceRoot":"","sources":["../src/bundler.ts"],"names":[],"mappings":"AAwFA,wBAAsB,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,OAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA4DlG"}
package/dist/bundler.js CHANGED
@@ -63,6 +63,20 @@ async function deleteWorkDir(workDir) {
63
63
  logger.warn(`Failed to remove temporary work dir "${workDir}": ${err.message}`);
64
64
  }
65
65
  }
66
+ async function prepareTypeCheckConfig(projectRoot, workDir) {
67
+ const realTsconfigPath = path.join(projectRoot, "tsconfig.json");
68
+ if (!existsSync(realTsconfigPath))
69
+ return;
70
+ const scratchConfig = {
71
+ extends: realTsconfigPath.replace(/\\/g, "/"),
72
+ compilerOptions: {
73
+ rootDir: projectRoot.replace(/\\/g, "/"),
74
+ },
75
+ include: [],
76
+ exclude: [],
77
+ };
78
+ await writeFile(path.join(workDir, "tsconfig.json"), JSON.stringify(scratchConfig, null, 2), "utf8");
79
+ }
66
80
  export async function bundleAgent(testSuitePaths, keep = false) {
67
81
  if (testSuitePaths.length === 0) {
68
82
  throw new Error("bundleAgent requires at least one test suite path.");
@@ -87,19 +101,19 @@ export async function bundleAgent(testSuitePaths, keep = false) {
87
101
  .join("\n");
88
102
  await rm(entrypointPath, { force: true });
89
103
  await writeFile(entrypointPath, agentSource.replace(IMPORT_MARKER, importStatements), "utf8");
104
+ await prepareTypeCheckConfig(projectRoot, workDir);
90
105
  const outfilePath = path.join(workDir, AGENT_BUNDLE_FILENAME);
91
106
  const fridaCompile = getFridaCompileBin(projectRoot);
92
107
  const args = fridaCompile.useLocal ? [entrypointPath, "-o", outfilePath] : ["frida-compile", entrypointPath, "-o", outfilePath];
93
108
  try {
94
109
  execFileSync(fridaCompile.path, args, {
95
- cwd: projectRoot,
110
+ cwd: workDir,
96
111
  shell: process.platform === "win32",
97
- encoding: "utf8",
112
+ stdio: "inherit",
98
113
  });
99
114
  }
100
115
  catch (err) {
101
- const stderr = err.stderr?.toString().trim();
102
- throw new Error(`frida-compile failed for entrypoint "${entrypointPath}" with suites [${testSuitePaths.join(", ")}]: ${stderr || err.message}`, { cause: err });
116
+ throw new Error(`frida-compile failed for entrypoint "${entrypointPath}" with suites [${testSuitePaths.join(", ")}] (see compiler output above for details)`, { cause: err });
103
117
  }
104
118
  logger.info(`Agent bundle sucessfully created and saved at ${outfilePath}.`);
105
119
  return await readFile(outfilePath, "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frida-test",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "license": "GPL-3.0-only",
6
6
  "homepage": "https://github.com/bernhste/frida-test/blob/main/README.md",
@@ -49,12 +49,9 @@
49
49
  "build": "tsc -b",
50
50
  "watch": "tsc -b --watch",
51
51
  "clean": "tsc -b --clean && rimraf dist",
52
- "test:android": "npm run build && frida-test -U -f 'com.google.android.dialer' ./tests/",
52
+ "test:android": "npm run build && npx frida-test -U -f 'com.google.android.dialer' ./tests/",
53
53
  "prepublishOnly": "npm run clean && npm run build && npm run test:android"
54
54
  },
55
- "allowScripts": {
56
- "frida": true
57
- },
58
55
  "dependencies": {
59
56
  "chalk": "^6.0.0",
60
57
  "frida-compile": "^19.0.5"
@@ -64,5 +61,8 @@
64
61
  "@types/node": "^26.4.0",
65
62
  "rimraf": "^6.1.3",
66
63
  "typescript": "^7.0.2"
64
+ },
65
+ "allowScripts": {
66
+ "frida@17.18.0": true
67
67
  }
68
68
  }
@@ -1,9 +1,14 @@
1
1
  type Matcher<T> = import("./matchers.js").Matcher<T>;
2
2
  type Spy = import("./matchers.js").Spy;
3
3
  type TestFn = import("./registry.js").TestFn;
4
+ type HookFn = import("./registry.js").HookFn;
4
5
 
5
6
  declare function describe(name: string, fn: TestFn): void;
6
7
  declare function it(name: string, fn: TestFn): void;
7
8
  declare function test(name: string, fn: TestFn): void;
9
+ declare function beforeEach(fn: HookFn): void;
10
+ declare function afterEach(fn: HookFn): void;
11
+ declare function beforeAll(fn: HookFn): void;
12
+ declare function afterAll(fn: HookFn): void;
8
13
  declare function expect<T>(actual: T): Matcher<T>;
9
14
  declare function spyOn<T extends object, K extends keyof T>(target: T, key: K): Spy;
@@ -1,13 +1,17 @@
1
1
  import { expect, spyOn, type Matcher, type Spy } from "./matchers.js";
2
- import type { TestFn } from "./registry.js";
3
- import { describe, it, test } from "./registry.js";
2
+ import type { HookFn, TestFn } from "./registry.js";
3
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, it, test } from "./registry.js";
4
4
 
5
5
  declare global {
6
6
  function describe(name: string, fn: TestFn): void;
7
7
  function it(name: string, fn: TestFn): void;
8
8
  function test(name: string, fn: TestFn): void;
9
+ function beforeEach(fn: HookFn): void;
10
+ function afterEach(fn: HookFn): void;
11
+ function beforeAll(fn: HookFn): void;
12
+ function afterAll(fn: HookFn): void;
9
13
  function expect<T>(actual: T): Matcher<T>;
10
14
  function spyOn<T extends object, K extends keyof T>(target: T, key: K): Spy;
11
15
  }
12
16
 
13
- Object.assign(globalThis, { describe, it, test, expect, spyOn });
17
+ Object.assign(globalThis, { describe, it, test, beforeEach, afterEach, beforeAll, afterAll, expect, spyOn });
@@ -1,6 +1,7 @@
1
1
  import { type AgentMessage, type RunSummary, type TestError, type TestResult, type TestStatus, type TestSuiteResult } from "frida-test/protocol.js";
2
2
 
3
3
  export type TestFn = () => void | Promise<void>;
4
+ export type HookFn = () => void | Promise<void>;
4
5
 
5
6
  type NodeKind = "describe" | "it";
6
7
 
@@ -9,13 +10,40 @@ interface TestSuiteNode {
9
10
  name: string;
10
11
  fn: TestFn;
11
12
  children?: TestSuiteNode[];
13
+ hooks?: HooksBag;
14
+ }
15
+
16
+ interface HooksBag {
17
+ beforeEach: HookFn[];
18
+ afterEach: HookFn[];
19
+ beforeAll: HookFn[];
20
+ afterAll: HookFn[];
21
+ }
22
+
23
+ interface EachHooks {
24
+ beforeEach: HookFn[];
25
+ afterEach: HookFn[];
26
+ }
27
+
28
+ function createHooksBag(): HooksBag {
29
+ return { beforeEach: [], afterEach: [], beforeAll: [], afterAll: [] };
30
+ }
31
+
32
+ interface Frame {
33
+ children: TestSuiteNode[];
34
+ hooks: HooksBag;
12
35
  }
13
36
 
14
37
  export const registry: TestSuiteNode[] = [];
15
- const stack: TestSuiteNode[][] = [];
38
+ const rootHooks: HooksBag = createHooksBag();
39
+ const stack: Frame[] = [{ children: registry, hooks: rootHooks }];
40
+
41
+ function currentFrame(): Frame {
42
+ return stack[stack.length - 1];
43
+ }
16
44
 
17
45
  function registerNode(kind: NodeKind, name: string, fn: TestFn): void {
18
- (stack.at(-1) ?? registry).push({ kind, name, fn });
46
+ currentFrame().children.push({ kind, name, fn });
19
47
  }
20
48
 
21
49
  export const describe = (name: string, fn: TestFn): void => registerNode("describe", name, fn);
@@ -23,6 +51,22 @@ export const describe = (name: string, fn: TestFn): void => registerNode("descri
23
51
  export const it = (name: string, fn: TestFn): void => registerNode("it", name, fn);
24
52
  export const test = it; // alias
25
53
 
54
+ export const beforeEach = (fn: HookFn): void => {
55
+ currentFrame().hooks.beforeEach.push(fn);
56
+ };
57
+
58
+ export const afterEach = (fn: HookFn): void => {
59
+ currentFrame().hooks.afterEach.push(fn);
60
+ };
61
+
62
+ export const beforeAll = (fn: HookFn): void => {
63
+ currentFrame().hooks.beforeAll.push(fn);
64
+ };
65
+
66
+ export const afterAll = (fn: HookFn): void => {
67
+ currentFrame().hooks.afterAll.push(fn);
68
+ };
69
+
26
70
  function serializeError(err: unknown, verbose: boolean): TestError {
27
71
  if (err instanceof Error) {
28
72
  return { message: err.message, stack: verbose ? err.stack : undefined };
@@ -30,6 +74,31 @@ function serializeError(err: unknown, verbose: boolean): TestError {
30
74
  return { message: String(err) };
31
75
  }
32
76
 
77
+ // Setup hooks (beforeEach/beforeAll): stop at the first failure since later hooks may depend on earlier ones.
78
+ async function runSetupHooks(hooks: HookFn[], verbose: boolean): Promise<TestError | undefined> {
79
+ for (const hook of hooks) {
80
+ try {
81
+ await hook();
82
+ } catch (err) {
83
+ return serializeError(err, verbose);
84
+ }
85
+ }
86
+ return undefined;
87
+ }
88
+
89
+ // Teardown hooks (afterEach/afterAll): run every hook regardless of earlier failures, to give cleanup a chance to run.
90
+ async function runTeardownHooks(hooks: HookFn[], verbose: boolean): Promise<TestError | undefined> {
91
+ let firstError: TestError | undefined;
92
+ for (const hook of hooks) {
93
+ try {
94
+ await hook();
95
+ } catch (err) {
96
+ firstError ??= serializeError(err, verbose);
97
+ }
98
+ }
99
+ return firstError;
100
+ }
101
+
33
102
  interface Counts {
34
103
  total: number;
35
104
  passed: number;
@@ -53,27 +122,28 @@ function withExpandLock<T>(fn: () => Promise<T>): Promise<T> {
53
122
  return run;
54
123
  }
55
124
 
56
- async function expand(node: TestSuiteNode, verbose: boolean): Promise<{ children: TestSuiteNode[]; error?: TestError }> {
125
+ async function expand(node: TestSuiteNode, verbose: boolean): Promise<{ children: TestSuiteNode[]; hooks: HooksBag; error?: TestError }> {
57
126
  return withExpandLock(async () => {
58
- const children: TestSuiteNode[] = [];
59
- stack.push(children);
127
+ const frame: Frame = { children: [], hooks: createHooksBag() };
128
+ stack.push(frame);
60
129
  try {
61
130
  await node.fn();
62
- return { children };
131
+ return { children: frame.children, hooks: frame.hooks };
63
132
  } catch (err) {
64
- return { children, error: serializeError(err, verbose) };
133
+ return { children: frame.children, hooks: frame.hooks, error: serializeError(err, verbose) };
65
134
  } finally {
66
135
  stack.pop();
67
136
  }
68
137
  });
69
138
  }
70
139
 
71
- async function runTestSuiteNode(node: TestSuiteNode, verbose: boolean): Promise<{ result: TestResult; counts: Counts }> {
140
+ async function runTestSuiteNode(node: TestSuiteNode, verbose: boolean, parentHooks: EachHooks): Promise<{ result: TestResult; counts: Counts }> {
72
141
  const start = Date.now();
73
142
 
74
143
  if (node.kind === "describe") {
75
- const { children: nodes, error } = await expand(node, verbose);
144
+ const { children: nodes, hooks, error } = await expand(node, verbose);
76
145
  node.children = nodes;
146
+ node.hooks = hooks;
77
147
 
78
148
  if (error) {
79
149
  const children: TestResult[] = nodes.map((child) => ({
@@ -93,42 +163,99 @@ async function runTestSuiteNode(node: TestSuiteNode, verbose: boolean): Promise<
93
163
  return { result, counts };
94
164
  }
95
165
 
166
+ const combinedHooks: EachHooks = {
167
+ beforeEach: [...parentHooks.beforeEach, ...hooks.beforeEach],
168
+ afterEach: [...hooks.afterEach, ...parentHooks.afterEach],
169
+ };
170
+
171
+ const beforeAllError = await runSetupHooks(hooks.beforeAll, verbose);
172
+ if (beforeAllError) {
173
+ const children: TestResult[] = nodes.map((child) => ({
174
+ name: child.name,
175
+ status: "failed",
176
+ durationMs: 0,
177
+ error: { message: `parent suite "${node.name}" failed before this test could run` },
178
+ }));
179
+ await runTeardownHooks(hooks.afterAll, verbose);
180
+ const counts: Counts = { total: 1 + children.length, passed: 0, failed: 1 + children.length };
181
+ const result: TestResult = {
182
+ name: node.name,
183
+ status: "failed",
184
+ durationMs: Date.now() - start,
185
+ error: beforeAllError,
186
+ children,
187
+ };
188
+ return { result, counts };
189
+ }
190
+
96
191
  const children: TestResult[] = [];
97
192
  let counts = ZERO_COUNTS;
98
193
  for (const child of node.children) {
99
- const childRun = await runTestSuiteNode(child, verbose);
194
+ const childRun = await runTestSuiteNode(child, verbose, combinedHooks);
100
195
  children.push(childRun.result);
101
196
  counts = addCounts(counts, childRun.counts);
102
197
  }
103
198
 
199
+ const afterAllError = await runTeardownHooks(hooks.afterAll, verbose);
200
+ if (afterAllError) {
201
+ children.push({ name: "afterAll hook", status: "failed", durationMs: 0, error: afterAllError });
202
+ counts = addCounts(counts, { total: 1, passed: 0, failed: 1 });
203
+ }
204
+
104
205
  const status: TestStatus = counts.failed > 0 ? "failed" : "passed";
105
206
  const result: TestResult = { name: node.name, status, durationMs: Date.now() - start, children };
106
207
  return { result, counts };
107
208
  }
108
209
 
109
210
  // Leaf test.
110
- try {
111
- await node.fn();
112
- const result: TestResult = { name: node.name, status: "passed", durationMs: Date.now() - start };
113
- return { result, counts: { total: 1, passed: 1, failed: 0 } };
114
- } catch (err) {
115
- const result: TestResult = {
116
- name: node.name,
117
- status: "failed",
118
- durationMs: Date.now() - start,
119
- error: serializeError(err, verbose),
120
- };
121
- return { result, counts: { total: 1, passed: 0, failed: 1 } };
211
+ const beforeEachError = await runSetupHooks(parentHooks.beforeEach, verbose);
212
+ let result: TestResult;
213
+ let counts: Counts;
214
+ if (beforeEachError) {
215
+ result = { name: node.name, status: "failed", durationMs: 0, error: beforeEachError };
216
+ counts = { total: 1, passed: 0, failed: 1 };
217
+ } else {
218
+ try {
219
+ await node.fn();
220
+ result = { name: node.name, status: "passed", durationMs: 0 };
221
+ counts = { total: 1, passed: 1, failed: 0 };
222
+ } catch (err) {
223
+ result = { name: node.name, status: "failed", durationMs: 0, error: serializeError(err, verbose) };
224
+ counts = { total: 1, passed: 0, failed: 1 };
225
+ }
122
226
  }
227
+
228
+ const afterEachError = await runTeardownHooks(parentHooks.afterEach, verbose);
229
+ if (afterEachError && result.status === "passed") {
230
+ result = { ...result, status: "failed", error: afterEachError };
231
+ counts = { total: 1, passed: 0, failed: 1 };
232
+ }
233
+
234
+ return { result: { ...result, durationMs: Date.now() - start }, counts };
123
235
  }
124
236
 
125
237
  export async function runTests(nodes: TestSuiteNode[], emit: (message: AgentMessage) => void, verbose: boolean = false): Promise<RunSummary> {
126
238
  const start = Date.now();
127
239
 
240
+ const beforeAllError = await runSetupHooks(rootHooks.beforeAll, verbose);
241
+ if (beforeAllError) {
242
+ const testSuitesResults: TestSuiteResult[] = nodes.map((node) => {
243
+ emit({ type: "test-suite-started", name: node.name });
244
+ const testResult: TestResult = { name: node.name, status: "failed", durationMs: 0, error: beforeAllError };
245
+ const suiteResult: TestSuiteResult = { name: node.name, testResult, status: "failed" };
246
+ emit({ type: "test-suite-finished", name: node.name, result: suiteResult });
247
+ return suiteResult;
248
+ });
249
+ await runTeardownHooks(rootHooks.afterAll, verbose);
250
+ return { total: nodes.length, passed: 0, failed: nodes.length, durationMs: Date.now() - start, testSuitesResults };
251
+ }
252
+
253
+ const parentHooks: EachHooks = { beforeEach: rootHooks.beforeEach, afterEach: rootHooks.afterEach };
254
+
128
255
  const settled = await Promise.allSettled(
129
256
  nodes.map(async (node) => {
130
257
  emit({ type: "test-suite-started", name: node.name });
131
- const { result: testResult, counts } = await runTestSuiteNode(node, verbose);
258
+ const { result: testResult, counts } = await runTestSuiteNode(node, verbose, parentHooks);
132
259
  const suiteResult: TestSuiteResult = { name: node.name, testResult, status: testResult.status };
133
260
  emit({ type: "test-suite-finished", name: node.name, result: suiteResult });
134
261
  return { suiteResult, counts };
@@ -155,5 +282,15 @@ export async function runTests(nodes: TestSuiteNode[], emit: (message: AgentMess
155
282
  counts = addCounts(counts, { total: 1, passed: 0, failed: 1 });
156
283
  });
157
284
 
285
+ const afterAllError = await runTeardownHooks(rootHooks.afterAll, verbose);
286
+ if (afterAllError) {
287
+ testSuitesResults.push({
288
+ name: "afterAll hook",
289
+ status: "failed",
290
+ testResult: { name: "afterAll hook", status: "failed", durationMs: 0, error: afterAllError },
291
+ });
292
+ counts = addCounts(counts, { total: 1, passed: 0, failed: 1 });
293
+ }
294
+
158
295
  return { ...counts, durationMs: Date.now() - start, testSuitesResults };
159
296
  }