@supatest/wdio-appium-reporter 0.0.2 → 0.0.3

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/dist/index.cjs CHANGED
@@ -6534,6 +6534,8 @@ var AttachmentUploader = class {
6534
6534
  var import_reporter = __toESM(require("@wdio/reporter"));
6535
6535
  var VIDEO_WAIT_TIMEOUT_MS = 6e4;
6536
6536
  var VIDEO_POLL_INTERVAL_MS = 1e3;
6537
+ var COORDINATOR_LOCK_TIMEOUT_MS = 1e4;
6538
+ var COORDINATOR_LOCK_POLL_MS = 50;
6537
6539
  var SupatestAppiumReporter = class extends import_reporter.default {
6538
6540
  reporterOptions;
6539
6541
  client;
@@ -6564,6 +6566,9 @@ var SupatestAppiumReporter = class extends import_reporter.default {
6564
6566
  runCompletePromise;
6565
6567
  runComplete = false;
6566
6568
  unregisterInterruptHandler;
6569
+ coordinatorPath;
6570
+ currentSpecKey = "unknown";
6571
+ expectedSpecs = [];
6567
6572
  testsForVideoUpload = [];
6568
6573
  // biome-ignore lint/suspicious/noExplicitAny: WDIOReporter options type is overly strict; we need to inject writeStream default
6569
6574
  constructor(options) {
@@ -6582,7 +6587,8 @@ var SupatestAppiumReporter = class extends import_reporter.default {
6582
6587
  timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
6583
6588
  dryRun: options.dryRun ?? process.env.SUPATEST_DRY_RUN === "true",
6584
6589
  screenshotDir: options.screenshotDir,
6585
- videoDir: options.videoDir
6590
+ videoDir: options.videoDir,
6591
+ runGroupId: options.runGroupId || process.env.SUPATEST_RUN_GROUP_ID
6586
6592
  };
6587
6593
  }
6588
6594
  onRunnerStart(runner) {
@@ -6630,6 +6636,10 @@ var SupatestAppiumReporter = class extends import_reporter.default {
6630
6636
  this.uploadLimit = pLimit(this.reporterOptions.maxConcurrentUploads);
6631
6637
  const capabilities = this.extractCapabilities(runner);
6632
6638
  const specs = runner.specs || [];
6639
+ this.expectedSpecs = this.getExpectedSpecs(runner);
6640
+ this.currentSpecKey = this.getCurrentSpecKey(runner);
6641
+ this.coordinatorPath = this.getCoordinatorPath(runner);
6642
+ const runSpecs = this.expectedSpecs.length > 0 ? this.expectedSpecs : specs;
6633
6643
  try {
6634
6644
  const runRequest = {
6635
6645
  projectId: this.reporterOptions.projectId,
@@ -6638,10 +6648,10 @@ var SupatestAppiumReporter = class extends import_reporter.default {
6638
6648
  version: this.getAppiumVersion(),
6639
6649
  framework: this.getFramework(runner),
6640
6650
  capabilities,
6641
- specs
6651
+ specs: runSpecs
6642
6652
  },
6643
6653
  testStats: {
6644
- totalFiles: specs.length,
6654
+ totalFiles: runSpecs.length,
6645
6655
  totalTests: 0,
6646
6656
  totalProjects: 1
6647
6657
  },
@@ -6649,15 +6659,14 @@ var SupatestAppiumReporter = class extends import_reporter.default {
6649
6659
  git: await this.getGitInfo(),
6650
6660
  rootDir: this.rootDir
6651
6661
  };
6652
- const response = await this.client.createRun(runRequest);
6653
- this.runId = response.runId;
6662
+ this.runId = await this.getOrCreateRun(runRequest);
6654
6663
  this.unregisterInterruptHandler = registerInterruptHandler(
6655
6664
  this.client,
6656
6665
  () => this.runId
6657
6666
  );
6658
6667
  const platform = capabilities.platformName || "unknown";
6659
6668
  const device = capabilities.deviceName || "unknown device";
6660
- logInfo(`Appium run ${this.runId} started [${platform} / ${device}] (${specs.length} spec files)`);
6669
+ logInfo(`Appium run ${this.runId} started [${platform} / ${device}] (${runSpecs.length} spec files)`);
6661
6670
  } catch (error) {
6662
6671
  const errorMsg = getErrorMessage2(error);
6663
6672
  this.errorCollector.recordError("RUN_CREATE", errorMsg, { error });
@@ -7046,6 +7055,11 @@ var SupatestAppiumReporter = class extends import_reporter.default {
7046
7055
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
7047
7056
  const startTime = this.startedAt ? new Date(this.startedAt).getTime() : 0;
7048
7057
  this.unregisterInterruptHandler?.();
7058
+ const shouldCompleteRun = await this.markSpecComplete();
7059
+ if (!shouldCompleteRun) {
7060
+ logInfo(`Appium run ${this.runId} still has pending spec files; skipping run completion in this worker`);
7061
+ return;
7062
+ }
7049
7063
  try {
7050
7064
  await this.client.completeRun(this.runId, {
7051
7065
  status: this.mapRunStatus(runner),
@@ -7179,6 +7193,151 @@ var SupatestAppiumReporter = class extends import_reporter.default {
7179
7193
  if (this.currentRunner?.specs?.[0]) return this.currentRunner.specs[0];
7180
7194
  return "unknown";
7181
7195
  }
7196
+ getExpectedSpecs(runner) {
7197
+ const configSpecs = this.flattenSpecEntries(runner.config?.specs);
7198
+ const expandedConfigSpecs = configSpecs.flatMap((spec) => this.expandSpecPattern(spec));
7199
+ const runnerSpecs = runner.specs || [];
7200
+ const specs = expandedConfigSpecs.length > 0 ? expandedConfigSpecs : runnerSpecs;
7201
+ return Array.from(new Set(specs.map((spec) => this.normalizeSpecPath(spec)))).sort();
7202
+ }
7203
+ flattenSpecEntries(value) {
7204
+ if (typeof value === "string") return [value];
7205
+ if (!Array.isArray(value)) return [];
7206
+ return value.flatMap((entry) => this.flattenSpecEntries(entry));
7207
+ }
7208
+ isGlobSpec(spec) {
7209
+ return /[*?[\]{}()!+@]/.test(spec);
7210
+ }
7211
+ expandSpecPattern(spec) {
7212
+ if (!this.isGlobSpec(spec)) return [spec];
7213
+ const absoluteSpec = import_node_path2.default.isAbsolute(spec) ? spec : import_node_path2.default.join(this.rootDir, spec);
7214
+ const firstGlobIndex = absoluteSpec.search(/[*?[\]{}()!+@]/);
7215
+ const basePrefix = firstGlobIndex >= 0 ? absoluteSpec.slice(0, firstGlobIndex) : absoluteSpec;
7216
+ const baseDir = /[\\/]$/.test(basePrefix) ? basePrefix.replace(/[\\/]$/, "") : import_node_path2.default.dirname(basePrefix);
7217
+ if (!import_node_fs.default.existsSync(baseDir)) return [];
7218
+ const extensionMatch = spec.match(/\.([a-zA-Z0-9]+)$/);
7219
+ const expectedExtension = extensionMatch ? `.${extensionMatch[1]}` : void 0;
7220
+ const files = [];
7221
+ const visit = (dir) => {
7222
+ for (const entry of import_node_fs.default.readdirSync(dir, { withFileTypes: true })) {
7223
+ const entryPath = import_node_path2.default.join(dir, entry.name);
7224
+ if (entry.isDirectory()) {
7225
+ visit(entryPath);
7226
+ continue;
7227
+ }
7228
+ if (!expectedExtension || entryPath.endsWith(expectedExtension)) {
7229
+ files.push(entryPath);
7230
+ }
7231
+ }
7232
+ };
7233
+ visit(baseDir);
7234
+ return files.sort();
7235
+ }
7236
+ normalizeSpecPath(spec) {
7237
+ const normalized = import_node_path2.default.isAbsolute(spec) ? import_node_path2.default.relative(this.rootDir, spec) : spec;
7238
+ return normalized.replace(/^[.][\\/]/, "").split(import_node_path2.default.sep).join("/");
7239
+ }
7240
+ getCurrentSpecKey(runner) {
7241
+ const specs = runner.specs || [];
7242
+ if (specs.length === 0) return runner.cid || "unknown";
7243
+ return specs.map((spec) => this.normalizeSpecPath(spec)).sort().join("|");
7244
+ }
7245
+ getCoordinatorPath(runner) {
7246
+ if (this.reporterOptions.dryRun) return void 0;
7247
+ const env = this.getEnvironmentInfo();
7248
+ const ciKey = env.ci ? [env.ci.provider, env.ci.runId, env.ci.jobId, env.ci.buildNumber].filter(Boolean).join(":") : "";
7249
+ const groupId = this.reporterOptions.runGroupId || ciKey || `local:${process.ppid}`;
7250
+ const configSpecs = this.flattenSpecEntries(runner.config?.specs).map((spec) => this.normalizeSpecPath(spec)).sort().join("|");
7251
+ const key = hashKey([
7252
+ this.reporterOptions.projectId,
7253
+ this.rootDir,
7254
+ groupId,
7255
+ configSpecs
7256
+ ].join("::"));
7257
+ return import_node_path2.default.join(import_node_os.default.tmpdir(), "supatest-wdio-appium-runs", `${key}.json`);
7258
+ }
7259
+ async getOrCreateRun(runRequest) {
7260
+ if (!this.coordinatorPath) {
7261
+ const response = await this.client.createRun(runRequest);
7262
+ return response.runId;
7263
+ }
7264
+ return this.withCoordinatorLock(async () => {
7265
+ const state = await this.readCoordinatorState();
7266
+ if (state.runId) return state.runId;
7267
+ const response = await this.client.createRun(runRequest);
7268
+ await this.writeCoordinatorState({
7269
+ runId: response.runId,
7270
+ startedAt: this.startedAt,
7271
+ expectedSpecs: this.expectedSpecs,
7272
+ completedSpecs: state.completedSpecs || []
7273
+ });
7274
+ return response.runId;
7275
+ });
7276
+ }
7277
+ async markSpecComplete() {
7278
+ if (!this.coordinatorPath) return true;
7279
+ return this.withCoordinatorLock(async () => {
7280
+ const state = await this.readCoordinatorState();
7281
+ const completed = new Set(state.completedSpecs || []);
7282
+ completed.add(this.currentSpecKey);
7283
+ const expected = state.expectedSpecs.length > 0 ? state.expectedSpecs : this.expectedSpecs;
7284
+ const complete = expected.length <= 1 || expected.every((spec) => completed.has(spec));
7285
+ await this.writeCoordinatorState({
7286
+ ...state,
7287
+ runId: state.runId || this.runId,
7288
+ startedAt: state.startedAt || this.startedAt,
7289
+ expectedSpecs: expected,
7290
+ completedSpecs: Array.from(completed).sort()
7291
+ });
7292
+ return complete;
7293
+ });
7294
+ }
7295
+ async readCoordinatorState() {
7296
+ if (!this.coordinatorPath) {
7297
+ return { expectedSpecs: this.expectedSpecs, completedSpecs: [] };
7298
+ }
7299
+ try {
7300
+ const raw = await import_node_fs.default.promises.readFile(this.coordinatorPath, "utf-8");
7301
+ const parsed = JSON.parse(raw);
7302
+ return {
7303
+ runId: parsed.runId,
7304
+ startedAt: parsed.startedAt,
7305
+ expectedSpecs: parsed.expectedSpecs || this.expectedSpecs,
7306
+ completedSpecs: parsed.completedSpecs || []
7307
+ };
7308
+ } catch {
7309
+ return { expectedSpecs: this.expectedSpecs, completedSpecs: [] };
7310
+ }
7311
+ }
7312
+ async writeCoordinatorState(state) {
7313
+ if (!this.coordinatorPath) return;
7314
+ await import_node_fs.default.promises.mkdir(import_node_path2.default.dirname(this.coordinatorPath), { recursive: true });
7315
+ await import_node_fs.default.promises.writeFile(this.coordinatorPath, JSON.stringify(state, null, 2));
7316
+ }
7317
+ async withCoordinatorLock(fn) {
7318
+ if (!this.coordinatorPath) return fn();
7319
+ await import_node_fs.default.promises.mkdir(import_node_path2.default.dirname(this.coordinatorPath), { recursive: true });
7320
+ const lockPath = `${this.coordinatorPath}.lock`;
7321
+ const start = Date.now();
7322
+ while (true) {
7323
+ let handle;
7324
+ try {
7325
+ handle = await import_node_fs.default.promises.open(lockPath, "wx");
7326
+ try {
7327
+ return await fn();
7328
+ } finally {
7329
+ await handle.close();
7330
+ await import_node_fs.default.promises.rm(lockPath, { force: true });
7331
+ }
7332
+ } catch (error) {
7333
+ if (error.code !== "EEXIST") throw error;
7334
+ if (Date.now() - start > COORDINATOR_LOCK_TIMEOUT_MS) {
7335
+ await import_node_fs.default.promises.rm(lockPath, { force: true });
7336
+ }
7337
+ await new Promise((resolve) => setTimeout(resolve, COORDINATOR_LOCK_POLL_MS));
7338
+ }
7339
+ }
7340
+ }
7182
7341
  extractTags(title) {
7183
7342
  const tags = [];
7184
7343
  const seenIndices = /* @__PURE__ */ new Set();