@matter/testing 0.13.1-alpha.0-20250502-43a54f780 → 0.13.1-alpha.0-20250504-87f265a2e

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.
Files changed (161) hide show
  1. package/bin/test.js +1 -2
  2. package/dist/cjs/chip/chip.d.ts +7 -2
  3. package/dist/cjs/chip/chip.d.ts.map +1 -1
  4. package/dist/cjs/chip/chip.js +20 -15
  5. package/dist/cjs/chip/chip.js.map +1 -1
  6. package/dist/cjs/chip/command-pipe.d.ts +2 -1
  7. package/dist/cjs/chip/command-pipe.d.ts.map +1 -1
  8. package/dist/cjs/chip/command-pipe.js +5 -2
  9. package/dist/cjs/chip/command-pipe.js.map +1 -1
  10. package/dist/cjs/chip/config.d.ts +2 -2
  11. package/dist/cjs/chip/config.d.ts.map +1 -1
  12. package/dist/cjs/chip/config.js +7 -6
  13. package/dist/cjs/chip/config.js.map +1 -1
  14. package/dist/cjs/chip/container-command-pipe.d.ts +2 -1
  15. package/dist/cjs/chip/container-command-pipe.d.ts.map +1 -1
  16. package/dist/cjs/chip/container-command-pipe.js +26 -24
  17. package/dist/cjs/chip/container-command-pipe.js.map +1 -1
  18. package/dist/cjs/chip/index.d.ts +1 -1
  19. package/dist/cjs/chip/index.d.ts.map +1 -1
  20. package/dist/cjs/chip/index.js +1 -1
  21. package/dist/cjs/chip/index.js.map +1 -1
  22. package/dist/cjs/chip/{pics-expression.d.ts → pics/expression.d.ts} +2 -2
  23. package/dist/cjs/chip/pics/expression.d.ts.map +1 -0
  24. package/dist/cjs/chip/{pics-expression.js → pics/expression.js} +6 -6
  25. package/dist/cjs/chip/pics/expression.js.map +6 -0
  26. package/dist/cjs/chip/{pics-file.d.ts → pics/file.d.ts} +10 -5
  27. package/dist/cjs/chip/pics/file.d.ts.map +1 -0
  28. package/dist/cjs/chip/{pics-file.js → pics/file.js} +21 -13
  29. package/dist/cjs/chip/pics/file.js.map +6 -0
  30. package/dist/cjs/chip/pics/index.d.ts +10 -0
  31. package/dist/cjs/chip/pics/index.d.ts.map +1 -0
  32. package/dist/cjs/chip/pics/index.js +27 -0
  33. package/dist/cjs/chip/pics/index.js.map +6 -0
  34. package/dist/cjs/chip/pics/source.d.ts +52 -0
  35. package/dist/cjs/chip/pics/source.d.ts.map +1 -0
  36. package/dist/cjs/chip/pics/source.js +117 -0
  37. package/dist/cjs/chip/pics/source.js.map +6 -0
  38. package/dist/cjs/chip/pics/values.d.ts +9 -0
  39. package/dist/cjs/chip/pics/values.d.ts.map +1 -0
  40. package/dist/cjs/chip/pics/values.js +22 -0
  41. package/dist/cjs/chip/pics/values.js.map +6 -0
  42. package/dist/cjs/chip/python-test.d.ts.map +1 -1
  43. package/dist/cjs/chip/python-test.js +14 -7
  44. package/dist/cjs/chip/python-test.js.map +1 -1
  45. package/dist/cjs/chip/state.d.ts +3 -2
  46. package/dist/cjs/chip/state.d.ts.map +1 -1
  47. package/dist/cjs/chip/state.js +38 -25
  48. package/dist/cjs/chip/state.js.map +1 -1
  49. package/dist/cjs/chip/yaml-test.d.ts +1 -1
  50. package/dist/cjs/chip/yaml-test.d.ts.map +1 -1
  51. package/dist/cjs/chip/yaml-test.js +5 -1
  52. package/dist/cjs/chip/yaml-test.js.map +1 -1
  53. package/dist/cjs/cli.d.ts.map +1 -1
  54. package/dist/cjs/cli.js +18 -3
  55. package/dist/cjs/cli.js.map +2 -2
  56. package/dist/cjs/device/subject.d.ts +4 -0
  57. package/dist/cjs/device/subject.d.ts.map +1 -1
  58. package/dist/cjs/failure-reporter.js +4 -6
  59. package/dist/cjs/failure-reporter.js.map +1 -1
  60. package/dist/cjs/print-report.d.ts +3 -0
  61. package/dist/cjs/print-report.d.ts.map +1 -1
  62. package/dist/cjs/print-report.js +13 -16
  63. package/dist/cjs/print-report.js.map +2 -2
  64. package/dist/cjs/test-descriptor.d.ts +18 -13
  65. package/dist/cjs/test-descriptor.d.ts.map +1 -1
  66. package/dist/cjs/test-descriptor.js +48 -49
  67. package/dist/cjs/test-descriptor.js.map +2 -2
  68. package/dist/esm/chip/chip.d.ts +7 -2
  69. package/dist/esm/chip/chip.d.ts.map +1 -1
  70. package/dist/esm/chip/chip.js +20 -15
  71. package/dist/esm/chip/chip.js.map +1 -1
  72. package/dist/esm/chip/command-pipe.d.ts +2 -1
  73. package/dist/esm/chip/command-pipe.d.ts.map +1 -1
  74. package/dist/esm/chip/command-pipe.js +5 -2
  75. package/dist/esm/chip/command-pipe.js.map +1 -1
  76. package/dist/esm/chip/config.d.ts +2 -2
  77. package/dist/esm/chip/config.d.ts.map +1 -1
  78. package/dist/esm/chip/config.js +7 -6
  79. package/dist/esm/chip/config.js.map +1 -1
  80. package/dist/esm/chip/container-command-pipe.d.ts +2 -1
  81. package/dist/esm/chip/container-command-pipe.d.ts.map +1 -1
  82. package/dist/esm/chip/container-command-pipe.js +26 -24
  83. package/dist/esm/chip/container-command-pipe.js.map +1 -1
  84. package/dist/esm/chip/index.d.ts +1 -1
  85. package/dist/esm/chip/index.d.ts.map +1 -1
  86. package/dist/esm/chip/index.js +1 -1
  87. package/dist/esm/chip/{pics-expression.d.ts → pics/expression.d.ts} +2 -2
  88. package/dist/esm/chip/pics/expression.d.ts.map +1 -0
  89. package/dist/esm/chip/{pics-expression.js → pics/expression.js} +3 -3
  90. package/dist/esm/chip/pics/expression.js.map +6 -0
  91. package/dist/esm/chip/{pics-file.d.ts → pics/file.d.ts} +10 -5
  92. package/dist/esm/chip/pics/file.d.ts.map +1 -0
  93. package/dist/esm/chip/{pics-file.js → pics/file.js} +18 -10
  94. package/dist/esm/chip/pics/file.js.map +6 -0
  95. package/dist/esm/chip/pics/index.d.ts +10 -0
  96. package/dist/esm/chip/pics/index.d.ts.map +1 -0
  97. package/dist/esm/chip/pics/index.js +10 -0
  98. package/dist/esm/chip/pics/index.js.map +6 -0
  99. package/dist/esm/chip/pics/source.d.ts +52 -0
  100. package/dist/esm/chip/pics/source.d.ts.map +1 -0
  101. package/dist/esm/chip/pics/source.js +97 -0
  102. package/dist/esm/chip/pics/source.js.map +6 -0
  103. package/dist/esm/chip/pics/values.d.ts +9 -0
  104. package/dist/esm/chip/pics/values.d.ts.map +1 -0
  105. package/dist/esm/chip/pics/values.js +6 -0
  106. package/dist/esm/chip/pics/values.js.map +6 -0
  107. package/dist/esm/chip/python-test.d.ts.map +1 -1
  108. package/dist/esm/chip/python-test.js +14 -7
  109. package/dist/esm/chip/python-test.js.map +1 -1
  110. package/dist/esm/chip/state.d.ts +3 -2
  111. package/dist/esm/chip/state.d.ts.map +1 -1
  112. package/dist/esm/chip/state.js +39 -26
  113. package/dist/esm/chip/state.js.map +1 -1
  114. package/dist/esm/chip/yaml-test.d.ts +1 -1
  115. package/dist/esm/chip/yaml-test.d.ts.map +1 -1
  116. package/dist/esm/chip/yaml-test.js +5 -1
  117. package/dist/esm/chip/yaml-test.js.map +1 -1
  118. package/dist/esm/cli.d.ts.map +1 -1
  119. package/dist/esm/cli.js +19 -4
  120. package/dist/esm/cli.js.map +2 -2
  121. package/dist/esm/device/subject.d.ts +4 -0
  122. package/dist/esm/device/subject.d.ts.map +1 -1
  123. package/dist/esm/failure-reporter.js +4 -6
  124. package/dist/esm/failure-reporter.js.map +1 -1
  125. package/dist/esm/print-report.d.ts +3 -0
  126. package/dist/esm/print-report.d.ts.map +1 -1
  127. package/dist/esm/print-report.js +12 -15
  128. package/dist/esm/print-report.js.map +2 -2
  129. package/dist/esm/test-descriptor.d.ts +18 -13
  130. package/dist/esm/test-descriptor.d.ts.map +1 -1
  131. package/dist/esm/test-descriptor.js +48 -49
  132. package/dist/esm/test-descriptor.js.map +2 -2
  133. package/package.json +2 -2
  134. package/src/chip/chip.ts +31 -17
  135. package/src/chip/command-pipe.ts +6 -2
  136. package/src/chip/config.ts +9 -9
  137. package/src/chip/container-command-pipe.ts +28 -30
  138. package/src/chip/index.ts +1 -1
  139. package/src/chip/matter-js-pics.properties +7 -4
  140. package/src/chip/{pics-expression.ts → pics/expression.ts} +4 -3
  141. package/src/chip/{pics-file.ts → pics/file.ts} +30 -13
  142. package/src/chip/pics/index.ts +10 -0
  143. package/src/chip/pics/source.ts +163 -0
  144. package/src/chip/pics/values.ts +9 -0
  145. package/src/chip/python-test.ts +18 -11
  146. package/src/chip/state.ts +56 -33
  147. package/src/chip/yaml-test.ts +6 -1
  148. package/src/cli.ts +29 -5
  149. package/src/device/subject.ts +4 -0
  150. package/src/failure-reporter.ts +4 -4
  151. package/src/print-report.ts +13 -18
  152. package/src/test-descriptor.ts +70 -61
  153. package/src/tsconfig.json +6 -2
  154. package/dist/cjs/chip/pics-expression.d.ts.map +0 -1
  155. package/dist/cjs/chip/pics-expression.js.map +0 -6
  156. package/dist/cjs/chip/pics-file.d.ts.map +0 -1
  157. package/dist/cjs/chip/pics-file.js.map +0 -6
  158. package/dist/esm/chip/pics-expression.d.ts.map +0 -1
  159. package/dist/esm/chip/pics-expression.js.map +0 -6
  160. package/dist/esm/chip/pics-file.d.ts.map +0 -1
  161. package/dist/esm/chip/pics-file.js.map +0 -6
@@ -19,21 +19,22 @@ export class ContainerCommandPipe extends CommandPipe {
19
19
  #deactivate?: () => void;
20
20
  #stopped?: Promise<void>;
21
21
 
22
- constructor(container: Container, subject: BackchannelCommand.Subject, appName: string) {
23
- super(subject, appName);
22
+ constructor(container: Container, subject: BackchannelCommand.Subject) {
23
+ super(subject, "/command-pipe.fifo");
24
24
  this.#container = container;
25
25
  }
26
26
 
27
27
  override async initialize() {
28
- // Many test files are hard-coded to /tmp so we swap out temp files to preserve state when we switch test
29
- // agents. This would overwrite our FIFO, however, so we symlink to the real FIFO to prevent it from being
30
- // overwritten
28
+ // We create a single FIFO for the container and symlink to the hard-coded name expected by CHIP
31
29
  await this.#container.createPipe(FIFO_PATH);
32
- await this.#container.exec(["ln", "-sf", "/command-pipe.fifo", this.filename]);
33
30
 
34
31
  this.#stopped = this.#processCommands();
35
32
  }
36
33
 
34
+ async installForApp(name: string) {
35
+ await this.#container.exec(["ln", "-sf", this.filename, ContainerCommandPipe.filenameFor(name)]);
36
+ }
37
+
37
38
  override async close() {
38
39
  this.#deactivate?.();
39
40
  this.#deactivate = undefined;
@@ -43,39 +44,36 @@ export class ContainerCommandPipe extends CommandPipe {
43
44
  }
44
45
 
45
46
  async #processCommands() {
46
- let terminal: Terminal<string> | undefined;
47
+ let commands: Terminal<string> | undefined;
47
48
  try {
48
- terminal = await this.#container.follow(FIFO_PATH);
49
+ commands = await this.#container.exec(
50
+ ["bash", "-c", `while true; do cat ${FIFO_PATH}; done`],
51
+ Terminal.StdoutLine,
52
+ );
49
53
 
50
- const iterator = terminal[Symbol.asyncIterator]();
51
-
52
- while (true) {
53
- let deactivator;
54
- const deactivated = new Promise<void>(resolve => {
55
- deactivator = this.#deactivate = resolve;
56
- });
54
+ const deactivated = new Promise<void>(resolve => {
55
+ this.#deactivate = resolve;
56
+ });
57
57
 
58
- const iteration = iterator.next().then(result => result.value as undefined | string);
58
+ const iterator = commands[Symbol.asyncIterator]();
59
59
 
60
- try {
61
- const line = await Promise.race([deactivated, iteration]);
62
- if (line === undefined) {
63
- break;
64
- }
65
-
66
- this.onData(line);
67
- } finally {
68
- if (this.#deactivate === deactivator) {
69
- this.#deactivate = undefined;
70
- }
60
+ while (true) {
61
+ const command = await Promise.race([
62
+ deactivated,
63
+ iterator.next().then(result => result.value as string),
64
+ ]);
65
+ if (command === undefined) {
66
+ break;
71
67
  }
68
+ this.onData(command);
72
69
  }
70
+ await deactivated;
73
71
  } finally {
74
- if (terminal) {
72
+ if (commands) {
75
73
  try {
76
- await terminal.close();
74
+ await commands.close();
77
75
  } catch (e) {
78
- console.warn(`Error closing FIFO listener for ${this.filename}:`, e);
76
+ console.warn(`Error closing command proxy for ${this.filename}:`, e);
79
77
  }
80
78
  }
81
79
 
package/src/chip/index.ts CHANGED
@@ -6,4 +6,4 @@
6
6
 
7
7
  export * from "./chip.js";
8
8
  export * from "./command-pipe.js";
9
- export * from "./pics-expression.js";
9
+ export * from "./pics/index.js";
@@ -24,7 +24,7 @@ DGGEN.S.A0006=0
24
24
  DGGEN.S.A0007=0
25
25
 
26
26
  # We do not support the "TestTrigger" command
27
- | DGGEN.S.C00.Rsp=0
27
+ DGGEN.S.C00.Rsp=0
28
28
 
29
29
  # We do not support the "TimeSnapshotResponse" command
30
30
  DGGEN.S.C03.Rsp=0
@@ -40,9 +40,12 @@ DGGEN.S.F00=0
40
40
  # We do not support the optional Battery Fault Change event
41
41
  PS.S.E01=0
42
42
 
43
- # We use an Ethernet Network commissioning cluster and so we do not have these two attributes
43
+ # We don't support ScanMaxTimeSeconds or ConnectMaxTimeSeconds attributes
44
44
  CNET.S.A0002..3=0
45
45
 
46
+ # We don't support ethernet networking
47
+ CNET.S.F02=0
48
+
46
49
  # We do not provide a Taglist on Descriptor cluster
47
50
  DESC.S.F00=0
48
51
 
@@ -55,8 +58,8 @@ BOOL.S.E00=1
55
58
  # We support "reachable" event on Basic Information
56
59
  BINFO.S.A0011=1
57
60
 
58
- # We don't support ScanMaxTimeSeconds or ConnectMaxTimeSeconds attributes
59
- CNET.S.A0002..3=0
61
+ # We do not support ConfigurationVersion attribute on Basic Information
62
+ BINFO.S.A0018=0
60
63
 
61
64
  # No level related features for CO2 measurement
62
65
  CDOCONC.S.F01..3=0
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { PicsFile } from "./pics-file.js";
7
+ import { PicsFile } from "./file.js";
8
8
 
9
9
  /**
10
10
  * A set of PICS specifiers, each of which must be true for the set to apply.
@@ -112,7 +112,8 @@ function parse(definition: string) {
112
112
 
113
113
  const result = parseExpr();
114
114
 
115
- if (tokenizer.peek() !== undefined) {
115
+ // Note - allow garbage trailing "(" as this seems to be a way to provide a hint as to purpose of expression
116
+ if (tokenizer.peek() !== undefined && tokenizer.peek()?.kind !== "(") {
116
117
  throw new InvalidPicsExpressionError(definition, tokenizer.peek());
117
118
  }
118
119
 
@@ -191,7 +192,7 @@ function test(ast: Ast, file: PicsFile): boolean {
191
192
  return !test(ast.operand, file);
192
193
 
193
194
  case "name":
194
- return file.values[ast.name] === "1";
195
+ return file.values[ast.name] === 1;
195
196
 
196
197
  case "&":
197
198
  return test(ast.lhs, file) && test(ast.rhs, file);
@@ -4,10 +4,10 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { readFileSync, writeFileSync } from "node:fs";
7
+ import { PicsValues } from "./values.js";
8
8
 
9
9
  /**
10
- * Manages Matter PICS files.
10
+ * In-memory Matter PICS file.
11
11
  *
12
12
  * Supports extended syntax for defining ranges of values of the form "NAMExx..yy=*" where xx and yy are hexadecimal
13
13
  * numbers specifying the start and end of a range (inclusive). These are expanded in {@link patch} which modifies the
@@ -18,13 +18,14 @@ import { readFileSync, writeFileSync } from "node:fs";
18
18
  */
19
19
  export class PicsFile {
20
20
  #lines: string[];
21
- #values?: Record<string, string>;
21
+ #values?: PicsValues;
22
22
 
23
- constructor(pathOrBody: string, inline = false) {
24
- if (inline === false) {
25
- pathOrBody = readFileSync(pathOrBody, "utf-8");
23
+ constructor(lines?: string | string[]) {
24
+ if (typeof lines === "string") {
25
+ this.#lines = lines.split("\n").map(l => l.trim());
26
+ } else {
27
+ this.#lines = lines ?? [];
26
28
  }
27
- this.#lines = pathOrBody.split("\n").map(l => l.trim());
28
29
  }
29
30
 
30
31
  get lines() {
@@ -33,7 +34,7 @@ export class PicsFile {
33
34
 
34
35
  get values() {
35
36
  if (!this.#values) {
36
- const values = {} as Record<string, string>;
37
+ const values = {} as PicsValues;
37
38
  for (const line of this.lines) {
38
39
  parseLine(line, values);
39
40
  }
@@ -51,7 +52,7 @@ export class PicsFile {
51
52
 
52
53
  const newLines = new Array<string>();
53
54
  for (const line of this.lines) {
54
- const lineValues = {} as Record<string, string>;
55
+ const lineValues = {} as PicsValues;
55
56
  if (!parseLine(line, lineValues)) {
56
57
  newLines.push(line);
57
58
  continue;
@@ -75,13 +76,15 @@ export class PicsFile {
75
76
  this.#values = undefined;
76
77
  this.#lines = newLines;
77
78
  }
79
+ }
78
80
 
79
- save(path: string) {
80
- writeFileSync(path, this.toString());
81
+ export namespace PicsFile {
82
+ export interface Values {
83
+ [key: string]: 0 | 1;
81
84
  }
82
85
  }
83
86
 
84
- function parseLine(line: string, values: Record<string, string>): boolean {
87
+ function parseLine(line: string, values: PicsValues): boolean {
85
88
  line = line.trim();
86
89
  if (line.startsWith("#")) {
87
90
  return false;
@@ -92,7 +95,21 @@ function parseLine(line: string, values: Record<string, string>): boolean {
92
95
  return false;
93
96
  }
94
97
 
95
- const [, key, value] = valueMatch;
98
+ const [, key, valueStr] = valueMatch;
99
+ let value: 0 | 1;
100
+ switch (valueStr) {
101
+ case "0":
102
+ value = 0;
103
+ break;
104
+
105
+ case "1":
106
+ value = 1;
107
+ break;
108
+
109
+ default:
110
+ return false;
111
+ }
112
+
96
113
  const rangeMatch = key.match(/^(\S+)\.\.([\da-f]+)$/i);
97
114
  if (!rangeMatch) {
98
115
  values[key] = value;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Project CHIP Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export * from "./expression.js";
8
+ export * from "./file.js";
9
+ export * from "./source.js";
10
+ export * from "./values.js";
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Project CHIP Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Package } from "#tools";
8
+ import { readFile } from "node:fs/promises";
9
+ import { isAbsolute } from "node:path";
10
+ import { State } from "../state.js";
11
+ import { PicsFile } from "./file.js";
12
+
13
+ const dataCache = new WeakMap<PicsSource, PicsFile>();
14
+ const filenameCache = new WeakMap<PicsFile, string>();
15
+
16
+ let nextFileNo = 1;
17
+
18
+ /**
19
+ * Source of PICS values.
20
+ */
21
+ export type PicsSource =
22
+ | PicsSource.Composite
23
+ | PicsSource.ChipFile
24
+ | PicsSource.LocalFile
25
+ | PicsSource.Lines
26
+ | PicsSource.Values;
27
+
28
+ export namespace PicsSource {
29
+ /**
30
+ * Load a {@link PicsFile} defined by a {@link PicsSource}.
31
+ *
32
+ * Caches results so a source always returns the same {@link PicsFile} instance.
33
+ */
34
+ export async function load(source: PicsSource): Promise<PicsFile> {
35
+ let file = dataCache.get(source);
36
+ if (file) {
37
+ return file;
38
+ }
39
+
40
+ switch (source.kind) {
41
+ case "composite":
42
+ for (const subsource of source.sources) {
43
+ const sourceFile = await load(subsource);
44
+ if (file) {
45
+ file.patch(sourceFile);
46
+ } else {
47
+ file = sourceFile;
48
+ }
49
+ }
50
+ if (!file) {
51
+ file = new PicsFile();
52
+ }
53
+ break;
54
+
55
+ case "chip":
56
+ file = new PicsFile(await State.container.read(source.name));
57
+ break;
58
+
59
+ case "file":
60
+ file = new PicsFile(await readFile(resolve(source.name), "utf-8"));
61
+ break;
62
+
63
+ case "lines":
64
+ file = new PicsFile(source.lines.split("\n").map(l => l.trim()));
65
+ break;
66
+
67
+ case "values":
68
+ file = new PicsFile(Object.entries(source.values).map(([key, value]) => `${key}=${value}`));
69
+ break;
70
+
71
+ default:
72
+ throw new Error(`Invalid PICS source kind "${(source as { kind: unknown }).kind}"`);
73
+ }
74
+
75
+ dataCache.set(source, file);
76
+
77
+ return file;
78
+ }
79
+
80
+ /**
81
+ * Save a {@link PicsFile} to the a {@link ChipFile} or {@link LocalFile}.
82
+ */
83
+ export async function save(target: ChipFile | LocalFile, file: PicsFile): Promise<void> {
84
+ switch (target.kind) {
85
+ case "chip":
86
+ await State.container.write(target.name, file.toString());
87
+ break;
88
+
89
+ case "file":
90
+ await State.container.write(resolve(target.name), file.toString());
91
+ break;
92
+
93
+ default:
94
+ throw new Error(`Invalid PICS target kind "${(target as { kind: unknown }).kind}"`);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Install a {@link PicsSource} into the test container.
100
+ *
101
+ * Returns the name of the file in the container.
102
+ *
103
+ * Results are cached so the same source always returns the same filename.
104
+ */
105
+ export async function install(file: PicsFile): Promise<string> {
106
+ let filename = filenameCache.get(file);
107
+ if (filename) {
108
+ return filename;
109
+ }
110
+
111
+ filename = `/pics-${nextFileNo++}.properties`;
112
+
113
+ try {
114
+ filenameCache.set(file, filename);
115
+ await save({ kind: "chip", name: filename }, file);
116
+ } catch (e) {
117
+ filenameCache.delete(file);
118
+ throw e;
119
+ }
120
+
121
+ return filename;
122
+ }
123
+
124
+ export interface Composite {
125
+ kind: "composite";
126
+ sources: Source[];
127
+ }
128
+
129
+ export interface ChipFile {
130
+ kind: "chip";
131
+ name: string;
132
+ }
133
+
134
+ export interface LocalFile {
135
+ kind: "file";
136
+ name: string;
137
+ }
138
+
139
+ export interface Lines {
140
+ kind: "lines";
141
+ lines: string;
142
+ }
143
+
144
+ export interface Values {
145
+ kind: "values";
146
+ values: Record<string, 0 | 1>;
147
+ }
148
+
149
+ export type Source = ChipFile | LocalFile | Lines | Values;
150
+ }
151
+
152
+ function resolve(path: string): string {
153
+ if (isAbsolute(path)) {
154
+ return path;
155
+ }
156
+
157
+ const testing = Package.tools.findPackage("@matter/testing");
158
+ if (testing.hasFile(path)) {
159
+ return testing.resolve(path);
160
+ }
161
+
162
+ return resolve(path);
163
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Project CHIP Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export interface PicsValues {
8
+ [name: string]: 0 | 1;
9
+ }
@@ -11,6 +11,7 @@ import { Terminal } from "../docker/terminal.js";
11
11
  import { TestFileDescriptor } from "../test-descriptor.js";
12
12
  import { parseStep } from "./chip-test-common.js";
13
13
  import { Constants, ContainerPaths } from "./config.js";
14
+ import { PicsSource } from "./pics/source.js";
14
15
 
15
16
  export class PythonTest extends BaseTest {
16
17
  /**
@@ -173,11 +174,12 @@ async function createCommand(descriptor: TestFileDescriptor, subject: Subject, e
173
174
  const command = ["python3", descriptor.path, ...Constants.PythonRunnerArgs];
174
175
 
175
176
  const args = scriptArgsOf(descriptor);
176
- if (args !== undefined) {
177
- command.push(...args);
178
- }
179
177
 
180
- command.push(...extraArgs);
178
+ command.push(...args, ...extraArgs);
179
+
180
+ if (!command.includes("--PICS")) {
181
+ command.push("--PICS", await PicsSource.install(subject.pics));
182
+ }
181
183
 
182
184
  const qrCodePos = command.indexOf("--qr-code");
183
185
  if (qrCodePos !== -1) {
@@ -188,13 +190,18 @@ async function createCommand(descriptor: TestFileDescriptor, subject: Subject, e
188
190
  }
189
191
 
190
192
  function scriptArgsOf(descriptor: TestFileDescriptor) {
191
- const scriptArgs = descriptor.config?.["script-args"];
192
- if (typeof scriptArgs !== "string") {
193
- return;
193
+ let args: string[] | undefined;
194
+
195
+ const predefined = descriptor.config?.["script-args"];
196
+ if (typeof predefined === "string") {
197
+ args = predefined.trim().split(/\s+/);
198
+ } else if (Array.isArray(predefined)) {
199
+ args = [...predefined];
200
+ }
201
+
202
+ if (descriptor.subpath) {
203
+ (args ?? (args = [])).push("--test-case", descriptor.subpath);
194
204
  }
195
205
 
196
- return scriptArgs
197
- .replace(/--(?:storage-path|commissioning-method|discriminator|passcode|trace-to|PICS)\s+\S+\s+/g, "")
198
- .trim()
199
- .split(/\s+/);
206
+ return args ?? [];
200
207
  }
package/src/chip/state.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { ansi, Package } from "#tools";
7
+ import { ansi } from "#tools";
8
8
  import { BackchannelCommand } from "../device/backchannel.js";
9
9
  import { Subject } from "../device/subject.js";
10
10
  import { Test } from "../device/test.js";
@@ -15,12 +15,13 @@ import { Image } from "../docker/image.js";
15
15
  import { Volume } from "../docker/volume.js";
16
16
  import { afterRun, beforeRun } from "../mocha.js";
17
17
  import type { TestRunner } from "../runner.js";
18
- import { TestDescriptor, TestFileDescriptor } from "../test-descriptor.js";
18
+ import { RootTestDescriptor, TestDescriptor, TestFileDescriptor } from "../test-descriptor.js";
19
19
  import { AccessoryServer } from "./accessory-server.js";
20
20
  import type { chip } from "./chip.js";
21
21
  import { Constants, ContainerPaths } from "./config.js";
22
22
  import { ContainerCommandPipe } from "./container-command-pipe.js";
23
- import { PicsFile } from "./pics-file.js";
23
+ import { PicsFile } from "./pics/file.js";
24
+ import { PicsSource } from "./pics/source.js";
24
25
  import { PythonTest } from "./python-test.js";
25
26
  import { YamlTest } from "./yaml-test.js";
26
27
 
@@ -35,18 +36,19 @@ const Values = {
35
36
  test: undefined as Test | undefined,
36
37
  mainContainer: undefined as Container | undefined,
37
38
  mdnsContainer: undefined as Container | undefined,
38
- pics: undefined as PicsFile | undefined,
39
+ defaultPics: undefined as PicsFile | undefined,
40
+ defaultPicsFilename: undefined as string | undefined,
39
41
  tests: undefined as TestDescriptor.Filesystem | undefined,
40
42
  initializedSubjects: new WeakSet<Subject>(),
41
43
  activeSubject: undefined as Subject | undefined,
42
44
  singleUseSubject: false,
43
- activePipes: new Set<string>(),
44
45
  closers: Array<() => Promise<void>>(),
45
46
  subjects: new Map<Subject.Factory, Record<string, Subject>>(),
46
47
  snapshots: new Map<Subject, {}>(),
47
48
  containerLifecycleInstalled: false,
48
49
  testMap: new Map<TestDescriptor, Test>(),
49
50
  pullBeforeTesting: true,
51
+ commandPipe: undefined as ContainerCommandPipe | undefined,
50
52
  };
51
53
 
52
54
  /**
@@ -113,12 +115,20 @@ export const State = {
113
115
  Values.pullBeforeTesting = value;
114
116
  },
115
117
 
116
- get pics() {
117
- if (Values.pics === undefined) {
118
+ get defaultPics() {
119
+ if (Values.defaultPics === undefined) {
118
120
  throw new Error("PICS not initialized");
119
121
  }
120
122
 
121
- return Values.pics;
123
+ return Values.defaultPics;
124
+ },
125
+
126
+ get defaultPicsFilename() {
127
+ if (Values.defaultPicsFilename === undefined) {
128
+ throw new Error("PICS not initialized");
129
+ }
130
+
131
+ return Values.defaultPicsFilename;
122
132
  },
123
133
 
124
134
  get tests() {
@@ -152,7 +162,7 @@ export const State = {
152
162
 
153
163
  progress.update("Initializing containers");
154
164
  try {
155
- const result = await initialize();
165
+ await initialize();
156
166
 
157
167
  const image = await State.container.image;
158
168
  const info = await image.inspect();
@@ -163,8 +173,6 @@ export const State = {
163
173
  progress.success(
164
174
  `Initialized CHIP ${ansi.bold(chipCommit)} image ${ansi.bold(imageVersion)} for ${ansi.bold(arch)}`,
165
175
  );
166
-
167
- return result;
168
176
  } catch (e) {
169
177
  progress.failure("Initializing containers");
170
178
  throw e;
@@ -240,20 +248,16 @@ export const State = {
240
248
  * Open a back-channel command pipe.
241
249
  */
242
250
  async openPipe(name: string) {
243
- if (Values.activePipes.has(name)) {
244
- return;
251
+ if (Values.commandPipe === undefined) {
252
+ Values.commandPipe = new ContainerCommandPipe(State.container, this);
253
+ await Values.commandPipe.initialize();
254
+ State.onClose(async () => {
255
+ await Values.commandPipe?.close();
256
+ Values.commandPipe = undefined;
257
+ });
245
258
  }
246
259
 
247
- const pipe = new ContainerCommandPipe(State.container, this, name);
248
-
249
- await pipe.initialize();
250
-
251
- Values.activePipes.add(name);
252
-
253
- State.onClose(async () => {
254
- await pipe.close();
255
- Values.activePipes.delete(name);
256
- });
260
+ await Values.commandPipe.installForApp(name);
257
261
  },
258
262
 
259
263
  /**
@@ -402,6 +406,7 @@ export const State = {
402
406
  */
403
407
  async function initialize() {
404
408
  await configureContainer();
409
+ await configureScripts();
405
410
  await configurePics();
406
411
  await configureTests();
407
412
  await configureNetwork();
@@ -465,19 +470,29 @@ async function configureContainer() {
465
470
  }
466
471
 
467
472
  /**
468
- * Create a PICS file in the container appropriate for matter.js.
473
+ * Monkey patch test scripts to work around bugs.
469
474
  */
470
- async function configurePics() {
471
- const ciPics = await State.container.read(ContainerPaths.chipPics);
472
- const pics = new PicsFile(ciPics, true);
475
+ async function configureScripts() {
476
+ // There is no ack on the command pipe. Writing to it is copied in a multitude of places (pending PR fixes some of
477
+ // this). Most places have a delay to try to compensate for lack of ack but in the "centralized" command writer the
478
+ // delay is only 1 ms. which is sometimes too short for us. Change to 20ms
479
+ await State.container.edit(
480
+ edit.sed("s/sleep(0.001)/sleep(.02)/"),
473
481
 
474
- const testing = Package.tools.findPackage("@matter/testing");
475
- const overrides = new PicsFile(testing.resolve(Constants.localPicsOverrideFile));
476
- pics.patch(overrides);
482
+ // This is the one we actually use
483
+ "/usr/local/lib/python3.12/dist-packages/chip/testing/matter_testing.py",
477
484
 
478
- Values.pics = pics;
485
+ // Patching here too just for completeness
486
+ "/src/python_testing/matter_testing_infrastructure/chip/testing/matter_testing.py",
487
+ );
488
+ }
479
489
 
480
- await State.container.write(ContainerPaths.matterJsPics, pics.toString());
490
+ /**
491
+ * Create a PICS file in the container appropriate for matter.js.
492
+ */
493
+ async function configurePics() {
494
+ Values.defaultPics = await PicsSource.load(Constants.defaultPics);
495
+ Values.defaultPicsFilename = await PicsSource.install(Values.defaultPics);
481
496
  }
482
497
 
483
498
  /**
@@ -487,10 +502,18 @@ async function configureTests() {
487
502
  const { container } = State;
488
503
 
489
504
  // Load test descriptors
490
- const descriptor = JSON.parse(await container.read(ContainerPaths.descriptorFile)) as TestDescriptor;
505
+ const descriptor = JSON.parse(await container.read(ContainerPaths.descriptorFile)) as RootTestDescriptor;
506
+
507
+ // Ensure this is a supported container version
508
+ if (descriptor.format !== TestDescriptor.CURRENT_FORMAT) {
509
+ throw new Error(`Invalid descriptor format "${descriptor.format}" (expected ${TestDescriptor.CURRENT_FORMAT})`);
510
+ }
511
+
512
+ // Ensure test descriptor isn't empty
491
513
  if (!Array.isArray(descriptor.members)) {
492
514
  throw new Error(`CHIP test descriptor has no members`);
493
515
  }
516
+
494
517
  Values.tests = TestDescriptor.Filesystem(descriptor);
495
518
  }
496
519