@limrun/api 0.48.1 → 0.48.2

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.
@@ -51,6 +51,28 @@ export type XcodeBuildExecRequest = {
51
51
  sdk?: 'iphonesimulator' | 'iphoneos' | 'watchsimulator' | 'watchos';
52
52
  configuration?: 'Debug' | 'Release';
53
53
  artifactName?: string;
54
+ /**
55
+ * xcodebuild action. 'build' (default) produces one .app; with a
56
+ * simulator attached it is installed there. 'build-for-testing' compiles
57
+ * the scheme's test targets and, with a simulator attached, runs them
58
+ * there, streaming per-case results as 'xctest' events; the exec exits
59
+ * non-zero when tests fail. Requires the iphonesimulator sdk.
60
+ */
61
+ action?: 'build' | 'build-for-testing';
62
+ /**
63
+ * Run only these tests, in xcodebuild's -only-testing format:
64
+ * Target[/Class[/method]]. Mutually exclusive with skipTesting; only
65
+ * valid with action 'build-for-testing'.
66
+ */
67
+ onlyTesting?: string[];
68
+ /** Skip these tests, in xcodebuild's -skip-testing format. */
69
+ skipTesting?: string[];
70
+ /**
71
+ * When false, 'build-for-testing' compiles the test targets but does not
72
+ * run them even when a simulator is attached. Servers without support
73
+ * ignore the field and still run the tests.
74
+ */
75
+ runTests?: boolean;
54
76
  };
55
77
  xcodegen?: {
56
78
  spec?: string;
@@ -234,6 +256,12 @@ export type ExecOptions = {
234
256
  apiUrl: string;
235
257
  token: string;
236
258
  log?: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
259
+ /**
260
+ * Called for each XCTest event as it streams from a build-for-testing run:
261
+ * one per finished test case, then the terminal summary. The same data
262
+ * lands accumulated on ExecResult.xctest.
263
+ */
264
+ onXctestEvent?: (event: XctestEvent) => void;
237
265
  };
238
266
 
239
267
  export type ExecResult = {
@@ -252,6 +280,13 @@ export type ExecResult = {
252
280
  * without a playstore request, or when the server predates the feature.
253
281
  */
254
282
  playstore?: PlaystoreEvent;
283
+ /**
284
+ * XCTest results streamed by a build-for-testing run: every finished case
285
+ * plus the terminal summary. Absent for plain builds and servers that
286
+ * predate the feature. A missing summary means the run died partway; the
287
+ * cases already streamed still stand.
288
+ */
289
+ xctest?: { cases: XctestCaseEvent[]; summary?: XctestSummaryEvent };
255
290
  /**
256
291
  * True when the client gave up waiting for the build's event stream. The
257
292
  * exit code is fabricated in that case; the remote build may still be
@@ -268,6 +303,32 @@ export type ExecResult = {
268
303
  incomplete?: { reason: 'timeout' | 'stream-lost' | 'stream-closed'; message: string };
269
304
  };
270
305
 
306
+ export type XctestCaseEvent = {
307
+ type: 'case';
308
+ /**
309
+ * The test class as XCTest reports it: module-qualified for Swift
310
+ * ('AppTests.LoginTests'), bare for Objective-C.
311
+ */
312
+ testClass: string;
313
+ method: string;
314
+ passed: boolean;
315
+ durationMs: number;
316
+ /** First failure message of a failed case. */
317
+ failureMessage?: string;
318
+ };
319
+
320
+ export type XctestSummaryEvent = {
321
+ type: 'summary';
322
+ passed: number;
323
+ failed: number;
324
+ /** True only once every test in scope ran to completion. */
325
+ planFinished: boolean;
326
+ /** Crash or per-target failure detail when the run degraded. */
327
+ error?: string;
328
+ };
329
+
330
+ export type XctestEvent = XctestCaseEvent | XctestSummaryEvent;
331
+
271
332
  export type AppStoreEvent = {
272
333
  /** 'unknown' means an App Store upload event arrived but its payload was unreadable. */
273
334
  state: 'uploading' | 'processing' | 'accepted' | 'failed' | 'unknown';
@@ -389,6 +450,13 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
389
450
  private detached = false;
390
451
  private appStoreEvent: AppStoreEvent | null = null;
391
452
  private playstoreEvent: PlaystoreEvent | null = null;
453
+ private xctestCases: XctestCaseEvent[] = [];
454
+ private xctestSummary: XctestSummaryEvent | null = null;
455
+ // Highest server-assigned event id already dispatched. The server numbers
456
+ // every frame and replays the stream from the start on reconnect; skipping
457
+ // ids at or below this mark drops the replayed prefix for every event type
458
+ // at once, and stays correct if the server ever honors Last-Event-ID.
459
+ private lastSeenEventId = -1;
392
460
  private readonly options: ExecOptions;
393
461
  private readonly log: (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => void;
394
462
 
@@ -555,7 +623,18 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
555
623
  let timeoutMs = 3600 * 1000;
556
624
  if (request.command === 'xcodebuild' && request.testflight) {
557
625
  timeoutMs += (Math.max(0, request.testflight.waitTimeoutSeconds ?? 0) + 900) * 1000;
558
- } else if (request.command === 'run') {
626
+ }
627
+ if (request.command === 'xcodebuild' && request.xcodebuild?.action === 'build-for-testing') {
628
+ // The test run happens after the build: products sync plus the suite,
629
+ // which the server caps at an hour per target. Two hours cover the
630
+ // common unit-plus-UI shape; the client cannot know the target count, so
631
+ // schemes with more targets may need a liveness-based deadline instead.
632
+ // Kept even with runTests: false: servers without run suppression still
633
+ // execute the suite, and the cap is an upper bound so a pure build
634
+ // finishes early regardless.
635
+ timeoutMs += 2 * 3600 * 1000;
636
+ }
637
+ if (request.command === 'run') {
559
638
  timeoutMs = (Math.max(1, request.timeoutSeconds ?? 3600) + 60) * 1000;
560
639
  }
561
640
  let exitCode: number;
@@ -629,6 +708,14 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
629
708
  ...('additionalMetadata' in request ? request.additionalMetadata ?? {} : {}),
630
709
  ...(this.appStoreEvent ? { appstore: this.appStoreEvent } : {}),
631
710
  ...(this.playstoreEvent ? { playstore: this.playstoreEvent } : {}),
711
+ ...(this.xctestCases.length > 0 || this.xctestSummary ?
712
+ {
713
+ xctest: {
714
+ cases: this.xctestCases,
715
+ ...(this.xctestSummary ? { summary: this.xctestSummary } : {}),
716
+ },
717
+ }
718
+ : {}),
632
719
  ...(timedOut ? { timedOut } : {}),
633
720
  ...(incomplete ? { incomplete } : {}),
634
721
  };
@@ -768,6 +855,15 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
768
855
  this.streamDeadSince = 0;
769
856
  proofOfLifeThisCycle = true;
770
857
  lastStreamError = undefined;
858
+ const eventId = message.id !== undefined ? parseInt(message.id, 10) : NaN;
859
+ if (!Number.isNaN(eventId)) {
860
+ if (eventId <= this.lastSeenEventId) {
861
+ // A reconnect replays the stream from the start; this frame was
862
+ // already dispatched on an earlier connection.
863
+ return;
864
+ }
865
+ this.lastSeenEventId = eventId;
866
+ }
771
867
  const data = typeof message.data === 'string' ? message.data : String(message.data ?? '');
772
868
  const eventType = message.event;
773
869
  if (eventType === 'command') {
@@ -795,6 +891,33 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
795
891
  this.playstoreEvent = { state: 'unknown' };
796
892
  this.log('warn', `SSE playstore event has invalid data: ${data}`);
797
893
  }
894
+ } else if (eventType === 'xctest') {
895
+ let event: XctestEvent | undefined;
896
+ try {
897
+ const parsed = JSON.parse(data) as XctestEvent;
898
+ if (parsed?.type === 'case') {
899
+ this.xctestCases.push(parsed);
900
+ event = parsed;
901
+ } else if (parsed?.type === 'summary') {
902
+ this.xctestSummary = parsed;
903
+ event = parsed;
904
+ } else {
905
+ // The simulator owns the frame vocabulary and may grow it; the
906
+ // callback's contract is cases and summaries only.
907
+ this.log('debug', `SSE xctest event of unknown type ignored: ${data}`);
908
+ }
909
+ } catch {
910
+ this.log('warn', `SSE xctest event has invalid data: ${data}`);
911
+ }
912
+ if (event) {
913
+ try {
914
+ this.options.onXctestEvent?.(event);
915
+ } catch (err) {
916
+ // The consumer's callback, not the frame: the run must not be
917
+ // derailed and the log must not blame the payload.
918
+ this.log('warn', `onXctestEvent callback threw: ${String(err)}`);
919
+ }
920
+ }
798
921
  } else if (eventType === 'exitCode') {
799
922
  const exitCode = parseInt(data, 10);
800
923
  if (Number.isNaN(exitCode)) {
package/src/index.ts CHANGED
@@ -41,6 +41,9 @@ export {
41
41
  type AppStoreUploadConfig,
42
42
  type PlaystoreEvent,
43
43
  type WebhookConfig,
44
+ type XctestCaseEvent,
45
+ type XctestEvent,
46
+ type XctestSummaryEvent,
44
47
  } from './exec-client';
45
48
  export {
46
49
  type XcodeCreateClientParams,
@@ -20,6 +20,7 @@ import {
20
20
  type ExecChildProcess,
21
21
  type ExecRequest,
22
22
  type WebhookConfig,
23
+ type XctestEvent,
23
24
  } from '../exec-client';
24
25
 
25
26
  export type { AppStoreUploadConfig, WebhookConfig } from '../exec-client';
@@ -117,6 +118,33 @@ export type XcodeProjectConfig = {
117
118
  * discovery picks the wrong bundle.
118
119
  */
119
120
  artifactName?: string;
121
+ /**
122
+ * xcodebuild action. 'build' (default) produces one .app; with a simulator
123
+ * attached it is installed there. 'build-for-testing' compiles the scheme's
124
+ * test targets and, with a simulator attached, runs them there: per-case
125
+ * results stream through XcodeBuildOptions.onXctestEvent and accumulate on
126
+ * ExecResult.xctest, and the exec exits non-zero when tests fail. Requires
127
+ * the iphonesimulator sdk. Older limbuild servers reject the field.
128
+ */
129
+ action?: 'build' | 'build-for-testing';
130
+ /**
131
+ * Run only these tests, in xcodebuild's -only-testing format:
132
+ * Target[/Class[/method]]. A bare target runs that target unfiltered;
133
+ * targets named by no entry are skipped. Mutually exclusive with
134
+ * skipTesting; only valid with action 'build-for-testing'.
135
+ */
136
+ onlyTesting?: string[];
137
+ /**
138
+ * Skip these tests, in xcodebuild's -skip-testing format. A bare target
139
+ * skips that whole target.
140
+ */
141
+ skipTesting?: string[];
142
+ /**
143
+ * When false, 'build-for-testing' compiles the test targets but does not
144
+ * run them even when a simulator is attached. Servers without support
145
+ * ignore the field and still run the tests.
146
+ */
147
+ runTests?: boolean;
120
148
  };
121
149
 
122
150
  export type XcodeGenConfig = {
@@ -260,6 +288,12 @@ export type XcodeBuildOptions = {
260
288
  * build. Older limbuild servers silently ignore this option.
261
289
  */
262
290
  webhook?: WebhookConfig;
291
+ /**
292
+ * Called for each XCTest event streamed by a build-for-testing run: one per
293
+ * finished test case, then the terminal summary. The same data accumulates
294
+ * on ExecResult.xctest.
295
+ */
296
+ onXctestEvent?: (event: XctestEvent) => void;
263
297
  };
264
298
 
265
299
  export type SimulatorInstallState =
@@ -843,6 +877,25 @@ export class XcodeInstances extends GeneratedXcodeInstances {
843
877
  if (options?.reactNative?.devServerURL && settings?.configuration === 'Release') {
844
878
  throw new Error('reactNative.devServerURL is only supported for Debug builds');
845
879
  }
880
+ if (settings?.onlyTesting?.length && settings?.skipTesting?.length) {
881
+ throw new Error('onlyTesting and skipTesting are mutually exclusive; pass one');
882
+ }
883
+ if (
884
+ (settings?.onlyTesting?.length || settings?.skipTesting?.length) &&
885
+ settings?.action !== 'build-for-testing'
886
+ ) {
887
+ throw new Error("onlyTesting/skipTesting require action: 'build-for-testing'");
888
+ }
889
+ if (settings?.runTests === false && settings?.action !== 'build-for-testing') {
890
+ throw new Error("runTests: false requires action: 'build-for-testing'");
891
+ }
892
+ if (
893
+ settings?.action === 'build-for-testing' &&
894
+ settings.sdk !== undefined &&
895
+ settings.sdk !== 'iphonesimulator'
896
+ ) {
897
+ throw new Error("action: 'build-for-testing' requires the iphonesimulator sdk");
898
+ }
846
899
  if (options?.buildSettings) {
847
900
  validateBuildSettings(options.buildSettings);
848
901
  }
@@ -862,6 +915,16 @@ export class XcodeInstances extends GeneratedXcodeInstances {
862
915
  ...(options?.webhook && { webhook: options.webhook }),
863
916
  };
864
917
 
918
+ // One options object for both exec call sites, so a future per-exec
919
+ // option cannot be added to one branch and silently dropped from the
920
+ // other.
921
+ const execOptions = {
922
+ apiUrl,
923
+ token,
924
+ log,
925
+ ...(options?.onXctestEvent && { onXctestEvent: options.onXctestEvent }),
926
+ };
927
+
865
928
  if (options?.upload && 'assetName' in options.upload) {
866
929
  const requestPromise = mintAssetUploadUrls(
867
930
  client.assets,
@@ -875,14 +938,14 @@ export class XcodeInstances extends GeneratedXcodeInstances {
875
938
  request.additionalMetadata = { signedDownloadUrl: asset.signedDownloadUrl };
876
939
  return request;
877
940
  });
878
- return exec(requestPromise, { apiUrl, token, log });
941
+ return exec(requestPromise, execOptions);
879
942
  }
880
943
 
881
944
  if (options?.upload && 'signedUploadUrl' in options.upload) {
882
945
  request.signedUploadUrl = options.upload.signedUploadUrl;
883
946
  }
884
947
 
885
- return exec(request, { apiUrl, token, log });
948
+ return exec(request, execOptions);
886
949
  },
887
950
 
888
951
  run(commandLine: string, options?: XcodeRunOptions): ExecChildProcess {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.48.1'; // x-release-please-version
1
+ export const VERSION = '0.48.2'; // x-release-please-version
package/version.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.48.1";
1
+ export declare const VERSION = "0.48.2";
2
2
  //# sourceMappingURL=version.d.mts.map
package/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.48.1";
1
+ export declare const VERSION = "0.48.2";
2
2
  //# sourceMappingURL=version.d.ts.map
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '0.48.1'; // x-release-please-version
4
+ exports.VERSION = '0.48.2'; // x-release-please-version
5
5
  //# sourceMappingURL=version.js.map
package/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.48.1'; // x-release-please-version
1
+ export const VERSION = '0.48.2'; // x-release-please-version
2
2
  //# sourceMappingURL=version.mjs.map