@tiden/reporter-commons 0.2.0 → 0.4.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/CHANGELOG.md +44 -0
- package/dist/client/reporter-api-models.d.ts +14 -0
- package/dist/client/services/result-transformer.js +10 -0
- package/dist/client/tiden-client.d.ts +11 -2
- package/dist/client/tiden-client.js +43 -4
- package/dist/core/reporter-factory.js +9 -0
- package/dist/internal/index.d.ts +5 -0
- package/dist/internal/index.js +13 -1
- package/dist/internal/repository.d.ts +31 -0
- package/dist/internal/repository.js +210 -0
- package/dist/internal/spec-path.d.ts +57 -0
- package/dist/internal/spec-path.js +172 -0
- package/dist/internal/test-identity.d.ts +29 -0
- package/dist/internal/test-identity.js +71 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/test-identity.d.ts +14 -0
- package/dist/models/test-identity.js +2 -0
- package/dist/models/test-result.d.ts +7 -0
- package/dist/models/test-result.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- **Test identity parts.** A result can carry `identity`: `{ language, repository, scope, name }`,
|
|
6
|
+
sent next to the unchanged legacy `signature`. The server builds the identity key from these
|
|
7
|
+
parts, so a reporter never normalizes titles itself. `ResultCreate.identity` is added to the
|
|
8
|
+
hand-written DTOs and mapped by `ResultTransformer`; `TestResultType.identity` carries it.
|
|
9
|
+
- **Repository resolution on the `/internal` entry point:** `resolveRepositoryContext`,
|
|
10
|
+
`buildTestIdentity`. The repository is `TIDEN_REPOSITORY`, then the CI variables
|
|
11
|
+
(`GITHUB_SERVER_URL`+`GITHUB_REPOSITORY`, `CI_SERVER_URL`+`CI_PROJECT_PATH`,
|
|
12
|
+
`BITBUCKET_GIT_HTTP_ORIGIN`, `BUILD_REPOSITORY_URI`), then the `origin` URL in the `.git/config`
|
|
13
|
+
of the nearest ancestor holding `.git` (a directory, or a file that points to the git directory:
|
|
14
|
+
worktrees and submodules). No subprocess, resolved once per process, sent raw: the server
|
|
15
|
+
normalizes it. The same ancestor is the root the test file is made relative to. No repository, or
|
|
16
|
+
a file outside that root, means no `identity` and one warning per process; the legacy
|
|
17
|
+
`signature` still identifies the test.
|
|
18
|
+
- **`clientMeta.gitRepository`** on the run, when a repository resolves.
|
|
19
|
+
- **Fallback for an older server.** A 400 whose body contains the gateway's
|
|
20
|
+
`unknown field "identity"` error makes the client resend the same batch (same result ids, so the
|
|
21
|
+
server still deduplicates) without `identity`, and stop sending it. Any other 400 is still a real
|
|
22
|
+
error.
|
|
23
|
+
- `CreateTestRunBody` gains `intentSessionId` and `intentBranch` in the hand-written DTO, which
|
|
24
|
+
the generated model already had.
|
|
25
|
+
|
|
26
|
+
## 0.3.0
|
|
27
|
+
|
|
28
|
+
- **`resolveFilePath` and `isUsableFilePath` on the `/internal` entry point.** The rule behind
|
|
29
|
+
every reporter's `fields.file_path`, which is the key the server joins a requirement's
|
|
30
|
+
repo-relative `repo_file` anchors against. Deliberately NOT `normalizeSpecPath`: that one
|
|
31
|
+
returns a path outside the root unchanged, which is right for a signature segment and wrong
|
|
32
|
+
for a join key — an absolute machine path or a virtual module id can never match an anchor,
|
|
33
|
+
so it would be a field that looks like a join key and silently never joins. `undefined`
|
|
34
|
+
means omit; omitting is not fabricating.
|
|
35
|
+
- **`resolveFilePath` resolves the root rather than trusting the string.** A relative root
|
|
36
|
+
(`..`, `.`, or `''` from a config file) matched no absolute test path, so every file was
|
|
37
|
+
omitted for the whole run while the warning told the user to set the variable they had just
|
|
38
|
+
set. Relative roots now resolve against cwd, `.`/`..` segments are collapsed on both sides,
|
|
39
|
+
and a symlinked root — `/tmp/x` while the runner emits `/private/tmp/x/...` on macOS — is
|
|
40
|
+
compared again via `realpath`, memoized and only after the plain comparison fails.
|
|
41
|
+
`tiden-phpunit-reporter`'s `FilePathResolver` already did this for the same macOS reason.
|
|
42
|
+
- **`normalizeSpecPath` and `resolveRootDir` are deliberately unchanged.** They feed the case
|
|
43
|
+
SIGNATURE, and changing how a root resolves would change the file segment of every signature
|
|
44
|
+
computed under a relative or symlinked root — forking the history of every case already
|
|
45
|
+
reported under the old spelling. A join key can be corrected; an identity cannot.
|
|
46
|
+
|
|
3
47
|
## 0.2.0
|
|
4
48
|
|
|
5
49
|
- **`rootDir` / `TIDEN_ROOT_DIR` option**, plus `normalizeSpecPath` and `resolveRootDir` on the
|
|
@@ -15,6 +15,8 @@ export interface CreateTestRunBody {
|
|
|
15
15
|
buildSha?: string;
|
|
16
16
|
startedAt?: string;
|
|
17
17
|
clientMeta?: Record<string, string>;
|
|
18
|
+
intentSessionId?: string;
|
|
19
|
+
intentBranch?: string;
|
|
18
20
|
}
|
|
19
21
|
export interface ReporterTestRun {
|
|
20
22
|
seqNum?: number;
|
|
@@ -62,10 +64,22 @@ export interface SuiteSegment {
|
|
|
62
64
|
title?: string;
|
|
63
65
|
externalId?: string;
|
|
64
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* `ResultCreate.identity`: the parts of a test's identity. Optional; a server
|
|
69
|
+
* that predates it rejects the field, which the client handles by resending
|
|
70
|
+
* the batch without it (see `TidenApiClient`).
|
|
71
|
+
*/
|
|
72
|
+
export interface ResultIdentity {
|
|
73
|
+
language?: string;
|
|
74
|
+
repository?: string;
|
|
75
|
+
scope?: string[];
|
|
76
|
+
name?: string[];
|
|
77
|
+
}
|
|
65
78
|
export interface ResultCreate {
|
|
66
79
|
id?: string;
|
|
67
80
|
title?: string;
|
|
68
81
|
signature?: string;
|
|
82
|
+
identity?: ResultIdentity;
|
|
69
83
|
externalId?: string;
|
|
70
84
|
testopsIds?: number[];
|
|
71
85
|
execution?: ResultExecution;
|
|
@@ -59,6 +59,16 @@ class ResultTransformer {
|
|
|
59
59
|
fields: result.fields,
|
|
60
60
|
defect: false,
|
|
61
61
|
...(result.signature != null ? { signature: result.signature } : {}),
|
|
62
|
+
...(result.identity
|
|
63
|
+
? {
|
|
64
|
+
identity: {
|
|
65
|
+
language: result.identity.language,
|
|
66
|
+
repository: result.identity.repository,
|
|
67
|
+
scope: [...result.identity.scope],
|
|
68
|
+
name: [...result.identity.name],
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
62
72
|
};
|
|
63
73
|
if (result.tags && result.tags.length > 0) {
|
|
64
74
|
model.fields = {
|
|
@@ -20,6 +20,11 @@ export declare class TidenApiClient implements IClient {
|
|
|
20
20
|
private readonly runService;
|
|
21
21
|
private readonly attachmentService;
|
|
22
22
|
private readonly resultTransformer;
|
|
23
|
+
/**
|
|
24
|
+
* Set once the server has rejected `identity` as an unknown field. From then
|
|
25
|
+
* on this client sends results without it. See `postResultsWithRetry`.
|
|
26
|
+
*/
|
|
27
|
+
private identityRejected;
|
|
23
28
|
constructor(logger: LoggerInterface, config: TidenOptionsType, environment: string | undefined, rootSuite: string | undefined);
|
|
24
29
|
createRun(): Promise<number>;
|
|
25
30
|
completeRun(runId: number): Promise<void>;
|
|
@@ -27,8 +32,12 @@ export declare class TidenApiClient implements IClient {
|
|
|
27
32
|
/** Public run report links have no Tiden equivalent in v1. */
|
|
28
33
|
enablePublicReport(_runId: number): Promise<void>;
|
|
29
34
|
uploadResults(runId: number, results: TestResultType[]): Promise<void>;
|
|
30
|
-
/** 429
|
|
35
|
+
/** 429 is retried (Retry-After honored, exp backoff, 30s cap). A 400 is
|
|
31
36
|
* terminal — resending an identical batch fails identically, so log
|
|
32
|
-
* every per-entry ReportError from details[] and re-throw
|
|
37
|
+
* every per-entry ReportError from details[] and re-throw — with one
|
|
38
|
+
* exception: a server that predates `identity` answers 400 with the
|
|
39
|
+
* gateway's `unknown field "identity"` error. Then the same batch (same
|
|
40
|
+
* result ids, so the server still deduplicates) is resent without it, and
|
|
41
|
+
* this client stops sending it. Any other 400 is a real error. */
|
|
33
42
|
private postResultsWithRetry;
|
|
34
43
|
}
|
|
@@ -26,6 +26,11 @@ class TidenApiClient {
|
|
|
26
26
|
runService;
|
|
27
27
|
attachmentService;
|
|
28
28
|
resultTransformer;
|
|
29
|
+
/**
|
|
30
|
+
* Set once the server has rejected `identity` as an unknown field. From then
|
|
31
|
+
* on this client sends results without it. See `postResultsWithRetry`.
|
|
32
|
+
*/
|
|
33
|
+
identityRejected = false;
|
|
29
34
|
constructor(logger, config, environment, rootSuite) {
|
|
30
35
|
this.logger = logger;
|
|
31
36
|
this.config = config;
|
|
@@ -55,16 +60,21 @@ class TidenApiClient {
|
|
|
55
60
|
const models = await Promise.all(results.map((result) => this.resultTransformer.transformWithDefect(result, (a) => this.attachmentService
|
|
56
61
|
.uploadAttachments(this.config.product, [a], this.config.uploadAttachments ?? true)
|
|
57
62
|
.then((hashes) => hashes[0] ?? ''), this.config.defect ?? false)));
|
|
58
|
-
await this.postResultsWithRetry(runId, models);
|
|
63
|
+
await this.postResultsWithRetry(runId, this.identityRejected ? models.map(withoutIdentity) : models);
|
|
59
64
|
}
|
|
60
65
|
catch (error) {
|
|
61
66
|
throw (0, api_error_handler_1.processError)(error, 'Error on uploading results', results);
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
|
-
/** 429
|
|
69
|
+
/** 429 is retried (Retry-After honored, exp backoff, 30s cap). A 400 is
|
|
65
70
|
* terminal — resending an identical batch fails identically, so log
|
|
66
|
-
* every per-entry ReportError from details[] and re-throw
|
|
67
|
-
|
|
71
|
+
* every per-entry ReportError from details[] and re-throw — with one
|
|
72
|
+
* exception: a server that predates `identity` answers 400 with the
|
|
73
|
+
* gateway's `unknown field "identity"` error. Then the same batch (same
|
|
74
|
+
* result ids, so the server still deduplicates) is resent without it, and
|
|
75
|
+
* this client stops sending it. Any other 400 is a real error. */
|
|
76
|
+
async postResultsWithRetry(runId, batch, maxRetries = 5) {
|
|
77
|
+
let models = batch;
|
|
68
78
|
let delay = 1000;
|
|
69
79
|
for (let attempt = 0;; attempt++) {
|
|
70
80
|
try {
|
|
@@ -83,6 +93,15 @@ class TidenApiClient {
|
|
|
83
93
|
delay = Math.min(delay * 2, 30_000);
|
|
84
94
|
continue;
|
|
85
95
|
}
|
|
96
|
+
if ((0, axios_1.isAxiosError)(error)
|
|
97
|
+
&& error.response?.status === 400
|
|
98
|
+
&& models.some((model) => model.identity !== undefined)
|
|
99
|
+
&& mentionsUnknownIdentityField(error.response.data)) {
|
|
100
|
+
this.identityRejected = true;
|
|
101
|
+
this.logger.log('The server does not accept test identity yet; reporting by signature only.');
|
|
102
|
+
models = models.map(withoutIdentity);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
86
105
|
if ((0, axios_1.isAxiosError)(error) && error.response?.status === 400) {
|
|
87
106
|
const details = error.response.data?.details ?? [];
|
|
88
107
|
for (const d of details) {
|
|
@@ -97,3 +116,23 @@ class TidenApiClient {
|
|
|
97
116
|
}
|
|
98
117
|
}
|
|
99
118
|
exports.TidenApiClient = TidenApiClient;
|
|
119
|
+
/** The gateway's wording for a JSON field the API does not know. */
|
|
120
|
+
const UNKNOWN_IDENTITY_FIELD = 'unknown field "identity"';
|
|
121
|
+
function withoutIdentity(model) {
|
|
122
|
+
const { identity: _identity, ...rest } = model;
|
|
123
|
+
return rest;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Whether an error body carries the unknown-`identity` error. The text sits in
|
|
127
|
+
* `message`, but a JSON-serialized body escapes its quotes, so strings are
|
|
128
|
+
* collected from the parsed body instead of searching the serialized form.
|
|
129
|
+
*/
|
|
130
|
+
function mentionsUnknownIdentityField(body, depth = 0) {
|
|
131
|
+
if (typeof body === 'string') {
|
|
132
|
+
return body.includes(UNKNOWN_IDENTITY_FIELD);
|
|
133
|
+
}
|
|
134
|
+
if (depth > 4 || body === null || typeof body !== 'object') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return Object.values(body).some((value) => mentionsUnknownIdentityField(value, depth + 1));
|
|
138
|
+
}
|
|
@@ -7,6 +7,8 @@ const env_1 = require("../env");
|
|
|
7
7
|
const disabled_exception_1 = require("../utils/disabled-exception");
|
|
8
8
|
const tiden_client_1 = require("../client/tiden-client");
|
|
9
9
|
const writer_1 = require("../writer");
|
|
10
|
+
const repository_1 = require("../internal/repository");
|
|
11
|
+
const spec_path_1 = require("../internal/spec-path");
|
|
10
12
|
/**
|
|
11
13
|
* Builds a mode-specific InternalReporterInterface. Throws DisabledException
|
|
12
14
|
* for `ModeEnum.off` so callers can distinguish "disabled-by-config" from a
|
|
@@ -51,6 +53,13 @@ class ReporterFactory {
|
|
|
51
53
|
os: this.hostData.system,
|
|
52
54
|
node: this.hostData.language,
|
|
53
55
|
};
|
|
56
|
+
// The raw remote of the repository under test, so the run itself is
|
|
57
|
+
// attributed even when no result carries identity parts. The server
|
|
58
|
+
// normalizes it. Omitted, not empty, when nothing resolves.
|
|
59
|
+
const gitRepository = (0, repository_1.resolveRepositoryContext)((0, spec_path_1.resolveRootDir)(options.rootDir)).repository;
|
|
60
|
+
if (gitRepository) {
|
|
61
|
+
tiden.clientMeta['gitRepository'] = gitRepository;
|
|
62
|
+
}
|
|
54
63
|
const apiClient = new tiden_client_1.TidenApiClient(this.logger, tiden, options.environment, options.rootSuite);
|
|
55
64
|
return new reporters_1.RunReporter(this.logger, apiClient, withState, tiden.product, tiden.batch?.size, tiden.run?.id);
|
|
56
65
|
}
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -8,3 +8,8 @@ export { filterPositiveIds } from './filter-positive-ids';
|
|
|
8
8
|
export { normalizeSuitePart } from './suite-normalizer';
|
|
9
9
|
export { normalizeSpecPath } from './spec-path';
|
|
10
10
|
export { resolveRootDir } from './spec-path';
|
|
11
|
+
export { resolveFilePath } from './spec-path';
|
|
12
|
+
export { isUsableFilePath } from './spec-path';
|
|
13
|
+
export { resolveRepositoryContext, resetRepositoryContext, REPOSITORY_ENV } from './repository';
|
|
14
|
+
export type { RepositoryContext } from './repository';
|
|
15
|
+
export { buildTestIdentity, resetIdentityWarning, JS_IDENTITY_LANGUAGE } from './test-identity';
|
package/dist/internal/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveRootDir = exports.normalizeSpecPath = exports.normalizeSuitePart = exports.filterPositiveIds = exports.parseTidenIdsFromString = exports.getFile = exports.extractAndCleanStep = exports.removeTidenIdsFromTitle = void 0;
|
|
3
|
+
exports.JS_IDENTITY_LANGUAGE = exports.resetIdentityWarning = exports.buildTestIdentity = exports.REPOSITORY_ENV = exports.resetRepositoryContext = exports.resolveRepositoryContext = exports.isUsableFilePath = exports.resolveFilePath = exports.resolveRootDir = exports.normalizeSpecPath = exports.normalizeSuitePart = exports.filterPositiveIds = exports.parseTidenIdsFromString = exports.getFile = exports.extractAndCleanStep = exports.removeTidenIdsFromTitle = void 0;
|
|
4
4
|
var title_1 = require("./title");
|
|
5
5
|
Object.defineProperty(exports, "removeTidenIdsFromTitle", { enumerable: true, get: function () { return title_1.removeTidenIdsFromTitle; } });
|
|
6
6
|
var step_parser_1 = require("./step-parser");
|
|
@@ -17,3 +17,15 @@ var spec_path_1 = require("./spec-path");
|
|
|
17
17
|
Object.defineProperty(exports, "normalizeSpecPath", { enumerable: true, get: function () { return spec_path_1.normalizeSpecPath; } });
|
|
18
18
|
var spec_path_2 = require("./spec-path");
|
|
19
19
|
Object.defineProperty(exports, "resolveRootDir", { enumerable: true, get: function () { return spec_path_2.resolveRootDir; } });
|
|
20
|
+
var spec_path_3 = require("./spec-path");
|
|
21
|
+
Object.defineProperty(exports, "resolveFilePath", { enumerable: true, get: function () { return spec_path_3.resolveFilePath; } });
|
|
22
|
+
var spec_path_4 = require("./spec-path");
|
|
23
|
+
Object.defineProperty(exports, "isUsableFilePath", { enumerable: true, get: function () { return spec_path_4.isUsableFilePath; } });
|
|
24
|
+
var repository_1 = require("./repository");
|
|
25
|
+
Object.defineProperty(exports, "resolveRepositoryContext", { enumerable: true, get: function () { return repository_1.resolveRepositoryContext; } });
|
|
26
|
+
Object.defineProperty(exports, "resetRepositoryContext", { enumerable: true, get: function () { return repository_1.resetRepositoryContext; } });
|
|
27
|
+
Object.defineProperty(exports, "REPOSITORY_ENV", { enumerable: true, get: function () { return repository_1.REPOSITORY_ENV; } });
|
|
28
|
+
var test_identity_1 = require("./test-identity");
|
|
29
|
+
Object.defineProperty(exports, "buildTestIdentity", { enumerable: true, get: function () { return test_identity_1.buildTestIdentity; } });
|
|
30
|
+
Object.defineProperty(exports, "resetIdentityWarning", { enumerable: true, get: function () { return test_identity_1.resetIdentityWarning; } });
|
|
31
|
+
Object.defineProperty(exports, "JS_IDENTITY_LANGUAGE", { enumerable: true, get: function () { return test_identity_1.JS_IDENTITY_LANGUAGE; } });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Explicit override. Wins over every CI variable and over `.git/config`. */
|
|
2
|
+
export declare const REPOSITORY_ENV = "TIDEN_REPOSITORY";
|
|
3
|
+
export interface RepositoryContext {
|
|
4
|
+
/** The raw remote, exactly as found. The server normalizes it. */
|
|
5
|
+
repository: string | undefined;
|
|
6
|
+
/** The nearest ancestor of the start directory that contains `.git`. */
|
|
7
|
+
root: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The repository a test run belongs to and the directory its files are
|
|
11
|
+
* measured from.
|
|
12
|
+
*
|
|
13
|
+
* Repository, first match wins:
|
|
14
|
+
* 1. `TIDEN_REPOSITORY`
|
|
15
|
+
* 2. CI environment: `GITHUB_SERVER_URL` + `GITHUB_REPOSITORY`,
|
|
16
|
+
* `CI_SERVER_URL` + `CI_PROJECT_PATH`, `BITBUCKET_GIT_HTTP_ORIGIN`,
|
|
17
|
+
* `BUILD_REPOSITORY_URI`
|
|
18
|
+
* 3. the `origin` URL in the `.git/config` of the repository root
|
|
19
|
+
*
|
|
20
|
+
* Root: the nearest ancestor of `startDir` (default `process.cwd()`) that
|
|
21
|
+
* contains `.git`. A directory, or a file that points to the real git
|
|
22
|
+
* directory (worktrees and submodules).
|
|
23
|
+
*
|
|
24
|
+
* No subprocess: `git` may be missing from a CI image, and spawning it for
|
|
25
|
+
* every test run costs more than reading one small file. The value is not
|
|
26
|
+
* normalized here; the server owns that rule, so a reporter can never drift
|
|
27
|
+
* from it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveRepositoryContext(startDir?: string): RepositoryContext;
|
|
30
|
+
/** Forget every resolved context. For tests; a real run never needs it. */
|
|
31
|
+
export declare function resetRepositoryContext(): void;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.REPOSITORY_ENV = void 0;
|
|
37
|
+
exports.resolveRepositoryContext = resolveRepositoryContext;
|
|
38
|
+
exports.resetRepositoryContext = resetRepositoryContext;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/** Explicit override. Wins over every CI variable and over `.git/config`. */
|
|
42
|
+
exports.REPOSITORY_ENV = 'TIDEN_REPOSITORY';
|
|
43
|
+
const SLASH = '/'.charCodeAt(0);
|
|
44
|
+
/**
|
|
45
|
+
* Resolved once per process and start directory. The answer cannot change
|
|
46
|
+
* while a test run is in flight, and every result of a run asks for it.
|
|
47
|
+
*/
|
|
48
|
+
const cache = new Map();
|
|
49
|
+
/**
|
|
50
|
+
* The repository a test run belongs to and the directory its files are
|
|
51
|
+
* measured from.
|
|
52
|
+
*
|
|
53
|
+
* Repository, first match wins:
|
|
54
|
+
* 1. `TIDEN_REPOSITORY`
|
|
55
|
+
* 2. CI environment: `GITHUB_SERVER_URL` + `GITHUB_REPOSITORY`,
|
|
56
|
+
* `CI_SERVER_URL` + `CI_PROJECT_PATH`, `BITBUCKET_GIT_HTTP_ORIGIN`,
|
|
57
|
+
* `BUILD_REPOSITORY_URI`
|
|
58
|
+
* 3. the `origin` URL in the `.git/config` of the repository root
|
|
59
|
+
*
|
|
60
|
+
* Root: the nearest ancestor of `startDir` (default `process.cwd()`) that
|
|
61
|
+
* contains `.git`. A directory, or a file that points to the real git
|
|
62
|
+
* directory (worktrees and submodules).
|
|
63
|
+
*
|
|
64
|
+
* No subprocess: `git` may be missing from a CI image, and spawning it for
|
|
65
|
+
* every test run costs more than reading one small file. The value is not
|
|
66
|
+
* normalized here; the server owns that rule, so a reporter can never drift
|
|
67
|
+
* from it.
|
|
68
|
+
*/
|
|
69
|
+
function resolveRepositoryContext(startDir) {
|
|
70
|
+
const start = path.resolve(startDir ?? process.cwd());
|
|
71
|
+
const cached = cache.get(start);
|
|
72
|
+
if (cached) {
|
|
73
|
+
return cached;
|
|
74
|
+
}
|
|
75
|
+
const found = findGitEntry(start);
|
|
76
|
+
const context = {
|
|
77
|
+
repository: repositoryFromEnv(process.env) ?? (found ? repositoryFromGit(found.dotGit) : undefined),
|
|
78
|
+
root: found?.root,
|
|
79
|
+
};
|
|
80
|
+
cache.set(start, context);
|
|
81
|
+
return context;
|
|
82
|
+
}
|
|
83
|
+
/** Forget every resolved context. For tests; a real run never needs it. */
|
|
84
|
+
function resetRepositoryContext() {
|
|
85
|
+
cache.clear();
|
|
86
|
+
}
|
|
87
|
+
function repositoryFromEnv(env) {
|
|
88
|
+
const value = (name) => {
|
|
89
|
+
const raw = env[name]?.trim();
|
|
90
|
+
return raw ? raw : undefined;
|
|
91
|
+
};
|
|
92
|
+
const explicit = value(exports.REPOSITORY_ENV);
|
|
93
|
+
if (explicit) {
|
|
94
|
+
return explicit;
|
|
95
|
+
}
|
|
96
|
+
const pairs = [
|
|
97
|
+
['GITHUB_SERVER_URL', 'GITHUB_REPOSITORY'],
|
|
98
|
+
['CI_SERVER_URL', 'CI_PROJECT_PATH'],
|
|
99
|
+
];
|
|
100
|
+
for (const [serverName, repoName] of pairs) {
|
|
101
|
+
const server = value(serverName);
|
|
102
|
+
const repo = value(repoName);
|
|
103
|
+
if (server && repo) {
|
|
104
|
+
return `${trimTrailingSlashes(server)}/${repo}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return value('BITBUCKET_GIT_HTTP_ORIGIN') ?? value('BUILD_REPOSITORY_URI');
|
|
108
|
+
}
|
|
109
|
+
/** Scanned, not `/\/+$/`: that pattern backtracks quadratically (see spec-path.ts). */
|
|
110
|
+
function trimTrailingSlashes(value) {
|
|
111
|
+
let end = value.length;
|
|
112
|
+
while (end > 0 && value.charCodeAt(end - 1) === SLASH) {
|
|
113
|
+
end -= 1;
|
|
114
|
+
}
|
|
115
|
+
return value.slice(0, end);
|
|
116
|
+
}
|
|
117
|
+
function findGitEntry(start) {
|
|
118
|
+
let dir = start;
|
|
119
|
+
for (;;) {
|
|
120
|
+
const dotGit = path.join(dir, '.git');
|
|
121
|
+
if (exists(dotGit)) {
|
|
122
|
+
return { root: dir, dotGit };
|
|
123
|
+
}
|
|
124
|
+
const parent = path.dirname(dir);
|
|
125
|
+
if (parent === dir) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
dir = parent;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function exists(candidate) {
|
|
132
|
+
try {
|
|
133
|
+
const stat = fs.statSync(candidate);
|
|
134
|
+
return stat.isDirectory() || stat.isFile();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function repositoryFromGit(dotGit) {
|
|
141
|
+
const gitDir = resolveGitDir(dotGit);
|
|
142
|
+
if (!gitDir) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
// A linked worktree keeps its own HEAD and index in `<main>/.git/worktrees/<n>`
|
|
146
|
+
// but shares the remotes with the main repository, named by `commondir`.
|
|
147
|
+
const commonDir = readTrimmed(path.join(gitDir, 'commondir'));
|
|
148
|
+
const configDir = commonDir ? path.resolve(gitDir, commonDir) : gitDir;
|
|
149
|
+
const config = readText(path.join(configDir, 'config'));
|
|
150
|
+
return config === undefined ? undefined : originUrl(config);
|
|
151
|
+
}
|
|
152
|
+
/** `.git` as a directory is the git directory; as a file it holds `gitdir: <path>`. */
|
|
153
|
+
function resolveGitDir(dotGit) {
|
|
154
|
+
try {
|
|
155
|
+
if (fs.statSync(dotGit).isDirectory()) {
|
|
156
|
+
return dotGit;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const content = readTrimmed(dotGit);
|
|
163
|
+
const prefix = 'gitdir:';
|
|
164
|
+
if (content === undefined || !content.startsWith(prefix)) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const target = content.slice(prefix.length).trim();
|
|
168
|
+
return target ? path.resolve(path.dirname(dotGit), target) : undefined;
|
|
169
|
+
}
|
|
170
|
+
function readText(file) {
|
|
171
|
+
try {
|
|
172
|
+
return fs.readFileSync(file, 'utf8');
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function readTrimmed(file) {
|
|
179
|
+
const text = readText(file);
|
|
180
|
+
return text === undefined ? undefined : (text.split(/\r?\n/, 1)[0] ?? '').trim();
|
|
181
|
+
}
|
|
182
|
+
const ORIGIN_SECTION = /^\[\s*remote\s+"origin"\s*\]/i;
|
|
183
|
+
/** The first `url` of `[remote "origin"]` in a git config file. */
|
|
184
|
+
function originUrl(config) {
|
|
185
|
+
let inOrigin = false;
|
|
186
|
+
for (const raw of config.split(/\r?\n/)) {
|
|
187
|
+
const line = raw.trim();
|
|
188
|
+
if (line.startsWith('[')) {
|
|
189
|
+
inOrigin = ORIGIN_SECTION.test(line);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!inOrigin || line.startsWith('#') || line.startsWith(';')) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const eq = line.indexOf('=');
|
|
196
|
+
if (eq < 0 || line.slice(0, eq).trim().toLowerCase() !== 'url') {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const value = unquote(line.slice(eq + 1).trim());
|
|
200
|
+
if (value) {
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
function unquote(value) {
|
|
207
|
+
return value.length >= 2 && value.startsWith('"') && value.endsWith('"')
|
|
208
|
+
? value.slice(1, -1)
|
|
209
|
+
: value;
|
|
210
|
+
}
|
|
@@ -10,3 +10,60 @@ export declare function normalizeSpecPath(fullPath: string, cwd?: string): strin
|
|
|
10
10
|
* own environment variable.
|
|
11
11
|
*/
|
|
12
12
|
export declare function resolveRootDir(explicit?: string | undefined): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The repo-relative source file of a test, for the `file_path` result field.
|
|
15
|
+
*
|
|
16
|
+
* Held to a stricter standard than `normalizeSpecPath`, which returns a path
|
|
17
|
+
* outside the root unchanged. `file_path` is the key the server joins a
|
|
18
|
+
* requirement's repo-relative `repo_file` anchors against, so an absolute
|
|
19
|
+
* machine path — or a virtual module id — can never match one: reporting it
|
|
20
|
+
* yields a field that looks like a join key and silently never joins.
|
|
21
|
+
* `undefined` means "omit the field", and the caller warns. Omitting is not
|
|
22
|
+
* fabricating; it is what the Go CLI's filemap and the PHPUnit reporter's
|
|
23
|
+
* FilePathResolver already do.
|
|
24
|
+
*
|
|
25
|
+
* Unlike `normalizeSpecPath` this is NOT purely path-shaped, because the root
|
|
26
|
+
* is user-supplied and three plausible spellings of the same directory would
|
|
27
|
+
* otherwise turn the feature off for a whole run:
|
|
28
|
+
* - a RELATIVE root (`..`, `.`, `''` from a config file) matches no absolute
|
|
29
|
+
* test path, so every file is omitted and the warning tells the user to set
|
|
30
|
+
* the variable they just set. Resolved against cwd.
|
|
31
|
+
* - UNNORMALIZED segments (`/repo/tests/../src/a.ts`) pass through verbatim
|
|
32
|
+
* and can never match an anchor. Collapsed.
|
|
33
|
+
* - a SYMLINKED root — `/tmp/checkout` on macOS while the runner emits
|
|
34
|
+
* `/private/tmp/checkout/...` — mismatches as a string. Compared again via
|
|
35
|
+
* realpath, and only when the plain comparison already failed, so the happy
|
|
36
|
+
* path touches the filesystem at most once per distinct root.
|
|
37
|
+
* `repos/phpunit`'s FilePathResolver calls realpath() on both sides for exactly
|
|
38
|
+
* the macOS /tmp reason; this brings the JS rule in line with it.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately NOT applied to `normalizeSpecPath`/`resolveRootDir`: those feed
|
|
41
|
+
* the case SIGNATURE, and changing how a root resolves would change the file
|
|
42
|
+
* segment of every signature computed under a relative or symlinked root —
|
|
43
|
+
* forking the history of every case already reported under the old spelling.
|
|
44
|
+
* A join key can be corrected; an identity cannot.
|
|
45
|
+
*
|
|
46
|
+
* Note what this still cannot detect: a path that resolves cleanly against the
|
|
47
|
+
* WRONG root. Playwright run from `tests/api` with no `TIDEN_ROOT_DIR` yields
|
|
48
|
+
* `Tests/v1/x.api.spec.ts` — well-formed, matching no anchor. Set
|
|
49
|
+
* `rootDir`/`TIDEN_ROOT_DIR` to the repository root wherever the runner's cwd
|
|
50
|
+
* is not already it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveFilePath(fullPath: string, root?: string): string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Whether a `file_path` the TEST set for itself could ever match a requirement
|
|
55
|
+
* anchor.
|
|
56
|
+
*
|
|
57
|
+
* The reporters keep a test-provided `file_path` — it may deliberately name the
|
|
58
|
+
* source file under test rather than the spec's own location — but the value is
|
|
59
|
+
* held to the same standard a derived one is: anchors are repo-relative, so an
|
|
60
|
+
* absolute path, an empty string, or one that escapes the root is unlinkable by
|
|
61
|
+
* construction. Keeping such a value would let a hand-written field do the one
|
|
62
|
+
* thing the resolver refuses to do, which is fabricate a link that never joins.
|
|
63
|
+
*
|
|
64
|
+
* This deliberately does NOT decide the larger question of whether a test
|
|
65
|
+
* should be able to override `file_path` at all — `tiden-phpunit-reporter`'s
|
|
66
|
+
* AttributeReader drops a hand-written `#[Field('file_path')]` outright. That
|
|
67
|
+
* divergence is a product decision, recorded in the PR, not settled here.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isUsableFilePath(value: string): boolean;
|
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.normalizeSpecPath = normalizeSpecPath;
|
|
4
37
|
exports.resolveRootDir = resolveRootDir;
|
|
38
|
+
exports.resolveFilePath = resolveFilePath;
|
|
39
|
+
exports.isUsableFilePath = isUsableFilePath;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
5
42
|
const env_enum_1 = require("../env/env-enum");
|
|
6
43
|
/**
|
|
7
44
|
* Normalizes an absolute spec-file path to a project-relative one, for use as
|
|
@@ -53,3 +90,138 @@ function resolveRootDir(explicit) {
|
|
|
53
90
|
const fromEnv = process.env[env_enum_1.EnvEnum.rootDir];
|
|
54
91
|
return explicit ?? (fromEnv !== undefined && fromEnv !== '' ? fromEnv : undefined);
|
|
55
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* The repo-relative source file of a test, for the `file_path` result field.
|
|
95
|
+
*
|
|
96
|
+
* Held to a stricter standard than `normalizeSpecPath`, which returns a path
|
|
97
|
+
* outside the root unchanged. `file_path` is the key the server joins a
|
|
98
|
+
* requirement's repo-relative `repo_file` anchors against, so an absolute
|
|
99
|
+
* machine path — or a virtual module id — can never match one: reporting it
|
|
100
|
+
* yields a field that looks like a join key and silently never joins.
|
|
101
|
+
* `undefined` means "omit the field", and the caller warns. Omitting is not
|
|
102
|
+
* fabricating; it is what the Go CLI's filemap and the PHPUnit reporter's
|
|
103
|
+
* FilePathResolver already do.
|
|
104
|
+
*
|
|
105
|
+
* Unlike `normalizeSpecPath` this is NOT purely path-shaped, because the root
|
|
106
|
+
* is user-supplied and three plausible spellings of the same directory would
|
|
107
|
+
* otherwise turn the feature off for a whole run:
|
|
108
|
+
* - a RELATIVE root (`..`, `.`, `''` from a config file) matches no absolute
|
|
109
|
+
* test path, so every file is omitted and the warning tells the user to set
|
|
110
|
+
* the variable they just set. Resolved against cwd.
|
|
111
|
+
* - UNNORMALIZED segments (`/repo/tests/../src/a.ts`) pass through verbatim
|
|
112
|
+
* and can never match an anchor. Collapsed.
|
|
113
|
+
* - a SYMLINKED root — `/tmp/checkout` on macOS while the runner emits
|
|
114
|
+
* `/private/tmp/checkout/...` — mismatches as a string. Compared again via
|
|
115
|
+
* realpath, and only when the plain comparison already failed, so the happy
|
|
116
|
+
* path touches the filesystem at most once per distinct root.
|
|
117
|
+
* `repos/phpunit`'s FilePathResolver calls realpath() on both sides for exactly
|
|
118
|
+
* the macOS /tmp reason; this brings the JS rule in line with it.
|
|
119
|
+
*
|
|
120
|
+
* Deliberately NOT applied to `normalizeSpecPath`/`resolveRootDir`: those feed
|
|
121
|
+
* the case SIGNATURE, and changing how a root resolves would change the file
|
|
122
|
+
* segment of every signature computed under a relative or symlinked root —
|
|
123
|
+
* forking the history of every case already reported under the old spelling.
|
|
124
|
+
* A join key can be corrected; an identity cannot.
|
|
125
|
+
*
|
|
126
|
+
* Note what this still cannot detect: a path that resolves cleanly against the
|
|
127
|
+
* WRONG root. Playwright run from `tests/api` with no `TIDEN_ROOT_DIR` yields
|
|
128
|
+
* `Tests/v1/x.api.spec.ts` — well-formed, matching no anchor. Set
|
|
129
|
+
* `rootDir`/`TIDEN_ROOT_DIR` to the repository root wherever the runner's cwd
|
|
130
|
+
* is not already it.
|
|
131
|
+
*/
|
|
132
|
+
function resolveFilePath(fullPath, root = process.cwd()) {
|
|
133
|
+
const file = collapse(fullPath);
|
|
134
|
+
const base = collapse(absoluteRoot(root));
|
|
135
|
+
const direct = relativeUnder(file, base);
|
|
136
|
+
if (direct !== undefined) {
|
|
137
|
+
return direct;
|
|
138
|
+
}
|
|
139
|
+
// Only now pay for the filesystem: a symlinked root (or file) is the one
|
|
140
|
+
// remaining way two spellings of the same location can differ as strings.
|
|
141
|
+
const realBase = collapse(realPathOrSelf(base));
|
|
142
|
+
const realFile = collapse(realPathOrSelf(file));
|
|
143
|
+
if (realBase === base && realFile === file) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
return relativeUnder(realFile, realBase);
|
|
147
|
+
}
|
|
148
|
+
/** `full` expressed relative to `base`, or undefined when it is not under it. */
|
|
149
|
+
function relativeUnder(full, base) {
|
|
150
|
+
const prefix = rootPrefix(base);
|
|
151
|
+
if (!full.startsWith(prefix)) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const relative = full.slice(prefix.length);
|
|
155
|
+
return relative === '' ? undefined : relative;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Backslashes to forward slashes, then `.`/`..` segments collapsed.
|
|
159
|
+
* `path.posix` regardless of host: the string is already slash-normalized, and
|
|
160
|
+
* a Windows path handed to a POSIX runner (or the reverse) must collapse the
|
|
161
|
+
* same way for the fixtures to mean anything.
|
|
162
|
+
*/
|
|
163
|
+
function collapse(value) {
|
|
164
|
+
return path.posix.normalize(value.replace(/\\/g, '/'));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A relative root resolved against cwd; an already-absolute one left alone.
|
|
168
|
+
*
|
|
169
|
+
* `path.resolve` is host-aware, so it cannot be applied unconditionally: on a
|
|
170
|
+
* POSIX runner it would turn the absolute Windows root `C:/repo` into
|
|
171
|
+
* `<cwd>/C:/repo`. The drive-letter test keeps that spelling absolute on both
|
|
172
|
+
* hosts. An empty root is relative by this test, so it resolves to cwd — which
|
|
173
|
+
* is what `normalizeSpecPath` already does with a falsy root, and closes the
|
|
174
|
+
* gap where `""` reported the absolute path minus its leading slash.
|
|
175
|
+
*/
|
|
176
|
+
function absoluteRoot(root) {
|
|
177
|
+
const slashed = root.replace(/\\/g, '/');
|
|
178
|
+
return isAbsoluteLike(slashed) ? slashed : path.resolve(root);
|
|
179
|
+
}
|
|
180
|
+
function isAbsoluteLike(value) {
|
|
181
|
+
return value.charCodeAt(0) === SLASH || /^[A-Za-z]:\//.test(value);
|
|
182
|
+
}
|
|
183
|
+
/** realpath, memoized per input; the input itself when it does not resolve. */
|
|
184
|
+
const realPathCache = new Map();
|
|
185
|
+
function realPathOrSelf(value) {
|
|
186
|
+
const cached = realPathCache.get(value);
|
|
187
|
+
if (cached !== undefined) {
|
|
188
|
+
return cached;
|
|
189
|
+
}
|
|
190
|
+
let resolved = value;
|
|
191
|
+
try {
|
|
192
|
+
resolved = fs.realpathSync(value);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Not on disk (a virtual module id, a deleted file, a root that does not
|
|
196
|
+
// exist). The raw string is the honest answer — same fall-back the PHP
|
|
197
|
+
// resolver takes when realpath() returns false.
|
|
198
|
+
}
|
|
199
|
+
realPathCache.set(value, resolved);
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether a `file_path` the TEST set for itself could ever match a requirement
|
|
204
|
+
* anchor.
|
|
205
|
+
*
|
|
206
|
+
* The reporters keep a test-provided `file_path` — it may deliberately name the
|
|
207
|
+
* source file under test rather than the spec's own location — but the value is
|
|
208
|
+
* held to the same standard a derived one is: anchors are repo-relative, so an
|
|
209
|
+
* absolute path, an empty string, or one that escapes the root is unlinkable by
|
|
210
|
+
* construction. Keeping such a value would let a hand-written field do the one
|
|
211
|
+
* thing the resolver refuses to do, which is fabricate a link that never joins.
|
|
212
|
+
*
|
|
213
|
+
* This deliberately does NOT decide the larger question of whether a test
|
|
214
|
+
* should be able to override `file_path` at all — `tiden-phpunit-reporter`'s
|
|
215
|
+
* AttributeReader drops a hand-written `#[Field('file_path')]` outright. That
|
|
216
|
+
* divergence is a product decision, recorded in the PR, not settled here.
|
|
217
|
+
*/
|
|
218
|
+
function isUsableFilePath(value) {
|
|
219
|
+
const collapsed = collapse(value);
|
|
220
|
+
if (collapsed === '' || collapsed === '.' || collapsed === '..') {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (isAbsoluteLike(collapsed)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return !collapsed.startsWith('../');
|
|
227
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TestIdentity } from '../models/test-identity';
|
|
2
|
+
/** The language token of every JavaScript reporter. */
|
|
3
|
+
export declare const JS_IDENTITY_LANGUAGE = "js";
|
|
4
|
+
/** Forget that a warning was printed. For tests; a real run never needs it. */
|
|
5
|
+
export declare function resetIdentityWarning(): void;
|
|
6
|
+
/**
|
|
7
|
+
* The identity parts of one test: the repository, the repo-relative POSIX
|
|
8
|
+
* file as scope, and the raw describe titles plus the test title as name.
|
|
9
|
+
*
|
|
10
|
+
* Titles are not lowercased, underscored or otherwise rewritten: the server
|
|
11
|
+
* builds the key from them, and a reporter-side normalizer is exactly what
|
|
12
|
+
* made jest and vitest disagree about one test. The trailing
|
|
13
|
+
* `(Tiden ID: N)` marker is the one thing removed from the test title; it is
|
|
14
|
+
* an explicit id, not part of the name, and adding one must not fork the
|
|
15
|
+
* test's history.
|
|
16
|
+
*
|
|
17
|
+
* `undefined` means "send no identity": no repository could be resolved, or
|
|
18
|
+
* the file is not under the repository root, or the test has no title. The
|
|
19
|
+
* legacy `signature` still identifies the test then. One warning per process
|
|
20
|
+
* names the first cause; a warning per test would bury the run log.
|
|
21
|
+
*
|
|
22
|
+
* @param startDir where to start looking for `.git`; defaults to `process.cwd()`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildTestIdentity(args: {
|
|
25
|
+
file: string | undefined;
|
|
26
|
+
describes: readonly string[];
|
|
27
|
+
title: string;
|
|
28
|
+
startDir?: string | undefined;
|
|
29
|
+
}): TestIdentity | undefined;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JS_IDENTITY_LANGUAGE = void 0;
|
|
4
|
+
exports.resetIdentityWarning = resetIdentityWarning;
|
|
5
|
+
exports.buildTestIdentity = buildTestIdentity;
|
|
6
|
+
const repository_1 = require("./repository");
|
|
7
|
+
const spec_path_1 = require("./spec-path");
|
|
8
|
+
const title_1 = require("./title");
|
|
9
|
+
/** The language token of every JavaScript reporter. */
|
|
10
|
+
exports.JS_IDENTITY_LANGUAGE = 'js';
|
|
11
|
+
let warned = false;
|
|
12
|
+
/** Forget that a warning was printed. For tests; a real run never needs it. */
|
|
13
|
+
function resetIdentityWarning() {
|
|
14
|
+
warned = false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The identity parts of one test: the repository, the repo-relative POSIX
|
|
18
|
+
* file as scope, and the raw describe titles plus the test title as name.
|
|
19
|
+
*
|
|
20
|
+
* Titles are not lowercased, underscored or otherwise rewritten: the server
|
|
21
|
+
* builds the key from them, and a reporter-side normalizer is exactly what
|
|
22
|
+
* made jest and vitest disagree about one test. The trailing
|
|
23
|
+
* `(Tiden ID: N)` marker is the one thing removed from the test title; it is
|
|
24
|
+
* an explicit id, not part of the name, and adding one must not fork the
|
|
25
|
+
* test's history.
|
|
26
|
+
*
|
|
27
|
+
* `undefined` means "send no identity": no repository could be resolved, or
|
|
28
|
+
* the file is not under the repository root, or the test has no title. The
|
|
29
|
+
* legacy `signature` still identifies the test then. One warning per process
|
|
30
|
+
* names the first cause; a warning per test would bury the run log.
|
|
31
|
+
*
|
|
32
|
+
* @param startDir where to start looking for `.git`; defaults to `process.cwd()`.
|
|
33
|
+
*/
|
|
34
|
+
function buildTestIdentity(args) {
|
|
35
|
+
const title = (0, title_1.removeTidenIdsFromTitle)(args.title);
|
|
36
|
+
if (title === '') {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const context = (0, repository_1.resolveRepositoryContext)(args.startDir);
|
|
40
|
+
if (!context.repository) {
|
|
41
|
+
warnOnce('no repository was found. Set TIDEN_REPOSITORY, run in CI with the standard '
|
|
42
|
+
+ 'repository variables, or run inside a git checkout with an "origin" remote.');
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (!context.root) {
|
|
46
|
+
warnOnce('no .git directory was found above the working directory, so test files cannot be made repo-relative.');
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const scope = args.file ? (0, spec_path_1.resolveFilePath)(args.file, context.root) : undefined;
|
|
50
|
+
if (scope === undefined) {
|
|
51
|
+
warnOnce(`${args.file ?? 'a test with no file'} is outside the repository root (${context.root}).`);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
language: exports.JS_IDENTITY_LANGUAGE,
|
|
56
|
+
repository: context.repository,
|
|
57
|
+
scope: [scope],
|
|
58
|
+
// An anonymous `describe('')` is not a level of the name. Playwright's
|
|
59
|
+
// own titlePath() drops it, and every framework must agree.
|
|
60
|
+
name: [...args.describes.filter((describe) => describe !== ''), title],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function warnOnce(reason) {
|
|
64
|
+
if (warned) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
warned = true;
|
|
68
|
+
console.warn(`tiden: not sending test identity — ${reason} `
|
|
69
|
+
+ 'Results are still reported, matched by their signature only. '
|
|
70
|
+
+ '(Reported once per process.)');
|
|
71
|
+
}
|
package/dist/models/index.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export type { Report } from './report';
|
|
|
9
9
|
export { CompoundError } from './error';
|
|
10
10
|
export type { ConfigurationGroup, ConfigurationItem, ConfigurationGroupResponse } from './configuration';
|
|
11
11
|
export type { TidenOptionsType, TidenApiType, TidenRunType, TidenBatchType, TidenConfigurationType, TidenConfigurationValueType } from './config/TidenOptionsType';
|
|
12
|
+
export type { TestIdentity } from './test-identity';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts a test's identity is made of, as sent in `ResultCreate.identity`.
|
|
3
|
+
* The server builds the identity key from them; a reporter never does.
|
|
4
|
+
*/
|
|
5
|
+
export interface TestIdentity {
|
|
6
|
+
/** Language token (`[a-z0-9-]{1,20}`); always `js` for the JavaScript reporters. */
|
|
7
|
+
language: string;
|
|
8
|
+
/** Raw repository remote or `host/owner/repo`; the server normalizes it. */
|
|
9
|
+
repository: string;
|
|
10
|
+
/** Structural location: the repo-relative POSIX test file. */
|
|
11
|
+
scope: string[];
|
|
12
|
+
/** Test name path: describe titles, then the test title, all raw. */
|
|
13
|
+
name: string[];
|
|
14
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TestStepType } from './test-step';
|
|
2
2
|
import { Attachment } from './attachment';
|
|
3
3
|
import { TestExecution } from './test-execution';
|
|
4
|
+
import { TestIdentity } from './test-identity';
|
|
4
5
|
/**
|
|
5
6
|
* Project code to test case IDs mapping for multi-project support.
|
|
6
7
|
* Key: project code (string), Value: array of test case IDs (numbers).
|
|
@@ -30,6 +31,12 @@ export declare class TestResultType {
|
|
|
30
31
|
message: string | null;
|
|
31
32
|
tags: string[];
|
|
32
33
|
preparedAttachments?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Identity parts sent next to the legacy `signature`. Absent when the
|
|
36
|
+
* reporter could not resolve a repository or the test file is outside the
|
|
37
|
+
* repository root. Deliberately not set by the constructor.
|
|
38
|
+
*/
|
|
39
|
+
identity?: TestIdentity | undefined;
|
|
33
40
|
constructor(title: string);
|
|
34
41
|
}
|
|
35
42
|
export interface Relation {
|
|
@@ -26,6 +26,12 @@ class TestResultType {
|
|
|
26
26
|
message;
|
|
27
27
|
tags;
|
|
28
28
|
preparedAttachments;
|
|
29
|
+
/**
|
|
30
|
+
* Identity parts sent next to the legacy `signature`. Absent when the
|
|
31
|
+
* reporter could not resolve a repository or the test file is outside the
|
|
32
|
+
* repository root. Deliberately not set by the constructor.
|
|
33
|
+
*/
|
|
34
|
+
identity;
|
|
29
35
|
constructor(title) {
|
|
30
36
|
this.id = '';
|
|
31
37
|
this.title = title;
|