frida-test 0.2.0 → 0.3.0

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
@@ -8,7 +8,16 @@ The following chapters explain how to write and run tests.
8
8
 
9
9
  ```sh
10
10
  npm install --save-dev frida-test
11
+ ```
12
+
13
+ After the installation, import the type `frida-test` into you project by adding the following configuration to the `tsconfig.json`:
11
14
 
15
+ ```json
16
+ {
17
+ "compilerOptions": {
18
+ "types": ["frida-test"]
19
+ }
20
+ }
12
21
  ```
13
22
 
14
23
  ## Writing Tests
@@ -29,7 +38,6 @@ describe('Classloader', () => {
29
38
  }).toThrow(new Error("Class 'badClass' is not available."));
30
39
  })
31
40
  });
32
-
33
41
  ```
34
42
 
35
43
  Tests can be nested to any depth and can be synchronous or asynchronous.
@@ -51,7 +59,6 @@ myProject/
51
59
  └── shared/
52
60
  ├── helper.ts
53
61
  └── helper.test.ts
54
-
55
62
  ```
56
63
 
57
64
  ## Matchers
@@ -79,13 +86,47 @@ myProject/
79
86
  > [!NOTE]
80
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/)
81
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
+
82
124
  ## Running Tests
83
125
 
84
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.
85
127
 
86
128
  ```sh
87
129
  frida-test [options] <dir...>
88
-
89
130
  ```
90
131
 
91
132
  ### Options
@@ -126,7 +167,6 @@ frida-test -U -N org.owasp.mastestapp.MASTestApp-iOS ./tests/ios ./tests/shared
126
167
 
127
168
  # Connect to a remote frida-server on HOST with authentication
128
169
  frida-test -H 192.168.1.10:27042 --token secret -p 4926 ./tests/shared
129
-
130
170
  ```
131
171
 
132
172
  ## Compile Agent
@@ -137,7 +177,6 @@ If you only want to compile this agent, use `frida-test-compile`:
137
177
 
138
178
  ```sh
139
179
  frida-test-compile [options] <dir...>
140
-
141
180
  ```
142
181
 
143
182
  | Option | Description |
@@ -153,5 +192,4 @@ frida-test-compile ./tests
153
192
 
154
193
  # Collects all tests in ./tests, compiles the frida-test agent, and stores it in ./frida-test-agent.js
155
194
  frida-test-compile ./tests -o ./frida-test-agent.js
156
-
157
195
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frida-test",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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.17.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
  }