@tiden/reporter-commons 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/config/config-validation-schema.d.ts +4 -0
- package/dist/config/config-validation-schema.js +4 -0
- package/dist/env/env-enum.d.ts +1 -0
- package/dist/env/env-enum.js +1 -0
- package/dist/env/env-to-config.js +6 -0
- package/dist/env/env-type.d.ts +1 -0
- package/dist/env/env-validation-schema.js +4 -0
- package/dist/internal/index.d.ts +4 -0
- package/dist/internal/index.js +9 -1
- package/dist/internal/spec-path.d.ts +69 -0
- package/dist/internal/spec-path.js +227 -0
- package/dist/options/options-type.d.ts +11 -0
- package/dist/reporters/shared/fallback-coordinator.d.ts +10 -0
- package/dist/reporters/shared/fallback-coordinator.js +12 -0
- package/dist/tiden.d.ts +11 -0
- package/dist/tiden.js +33 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- **`resolveFilePath` and `isUsableFilePath` on the `/internal` entry point.** The rule behind
|
|
6
|
+
every reporter's `fields.file_path`, which is the key the server joins a requirement's
|
|
7
|
+
repo-relative `repo_file` anchors against. Deliberately NOT `normalizeSpecPath`: that one
|
|
8
|
+
returns a path outside the root unchanged, which is right for a signature segment and wrong
|
|
9
|
+
for a join key — an absolute machine path or a virtual module id can never match an anchor,
|
|
10
|
+
so it would be a field that looks like a join key and silently never joins. `undefined`
|
|
11
|
+
means omit; omitting is not fabricating.
|
|
12
|
+
- **`resolveFilePath` resolves the root rather than trusting the string.** A relative root
|
|
13
|
+
(`..`, `.`, or `''` from a config file) matched no absolute test path, so every file was
|
|
14
|
+
omitted for the whole run while the warning told the user to set the variable they had just
|
|
15
|
+
set. Relative roots now resolve against cwd, `.`/`..` segments are collapsed on both sides,
|
|
16
|
+
and a symlinked root — `/tmp/x` while the runner emits `/private/tmp/x/...` on macOS — is
|
|
17
|
+
compared again via `realpath`, memoized and only after the plain comparison fails.
|
|
18
|
+
`tiden-phpunit-reporter`'s `FilePathResolver` already did this for the same macOS reason.
|
|
19
|
+
- **`normalizeSpecPath` and `resolveRootDir` are deliberately unchanged.** They feed the case
|
|
20
|
+
SIGNATURE, and changing how a root resolves would change the file segment of every signature
|
|
21
|
+
computed under a relative or symlinked root — forking the history of every case already
|
|
22
|
+
reported under the old spelling. A join key can be corrected; an identity cannot.
|
|
23
|
+
|
|
24
|
+
## 0.2.0
|
|
25
|
+
|
|
26
|
+
- **`rootDir` / `TIDEN_ROOT_DIR` option**, plus `normalizeSpecPath` and `resolveRootDir` on the
|
|
27
|
+
`/internal` entry point: the base a case signature's spec-file segment is resolved against,
|
|
28
|
+
defaulting to `process.cwd()`. Consumed by `@tiden/vitest-reporter` and `@tiden/jest-reporter`.
|
|
29
|
+
Env is read directly by `resolveRootDir` rather than through `OptionsResolver`, whose composed
|
|
30
|
+
result never reaches a framework reporter — resolving it any other way leaves `TIDEN_ROOT_DIR`
|
|
31
|
+
silently ignored while looking configured.
|
|
32
|
+
- **Fixed a polynomial ReDoS in `normalizeSpecPath`.** The root's trailing slashes were trimmed
|
|
33
|
+
with `/\/+$/`, which backtracks quadratically on a root of repeated slashes — and the root is
|
|
34
|
+
configurable (`rootDir` / `TIDEN_ROOT_DIR`). Measured at 546ms for a 40k-character root against
|
|
35
|
+
0ms for the scan that replaces it. Caught by CodeQL (`js/polynomial-redos`) before release; same
|
|
36
|
+
class as the step-marker parser fixed in 0.1.1.
|
|
37
|
+
- **Fixed: `TIDEN_FALLBACK` was silently ignored.** It was declared in `EnvEnum`, `EnvType` and the
|
|
38
|
+
env validation schema, but `envToConfig` never mapped it into `ConfigType` — so the value
|
|
39
|
+
validated and was then dropped. It decides what happens when `tiden` mode cannot start, which is
|
|
40
|
+
the disable path below.
|
|
41
|
+
- **A reporter that stops MID-RUN says so too.** `FallbackCoordinator.disable()` turned reporting
|
|
42
|
+
off after an upstream (and fallback) failure while logging only the error, not the consequence:
|
|
43
|
+
the suite carried on, every later result went nowhere, and the run ended green. A present-but-
|
|
44
|
+
rejected token reached the same silent failure as a missing one by a different path.
|
|
45
|
+
- **The credential list is no longer appended to every disabled message.** It is stated where a
|
|
46
|
+
missing setting is a live suspect, and omitted where the error above is the answer — a message
|
|
47
|
+
padded with the same tail every time stops being read.
|
|
48
|
+
- **`buildReporters` says why it disabled the reporter.** Both disabling paths — a config
|
|
49
|
+
`mode`/`fallback` of `off`, and a `tiden` mode that cannot start because one of the four
|
|
50
|
+
required settings is missing — used to set `disabled = true` with no output. A disabled reporter
|
|
51
|
+
is indistinguishable from a working one, which cost a user an hour. Each branch now announces
|
|
52
|
+
itself through `logReporterDisabled`, naming the reason, the remedy, and the credentials
|
|
53
|
+
`createRunReporter` requires. Deliberately `logger.log`, not `logDebug`.
|
|
54
|
+
|
|
3
55
|
## 0.1.1
|
|
4
56
|
|
|
5
57
|
- **Fixed a polynomial ReDoS in `extractAndCleanStep`** (CodeQL `js/polynomial-redos`, high).
|
package/dist/env/env-enum.d.ts
CHANGED
package/dist/env/env-enum.js
CHANGED
|
@@ -12,6 +12,7 @@ var EnvEnum;
|
|
|
12
12
|
EnvEnum["environment"] = "TIDEN_ENVIRONMENT";
|
|
13
13
|
EnvEnum["captureLogs"] = "TIDEN_CAPTURE_LOGS";
|
|
14
14
|
EnvEnum["rootSuite"] = "TIDEN_ROOT_SUITE";
|
|
15
|
+
EnvEnum["rootDir"] = "TIDEN_ROOT_DIR";
|
|
15
16
|
EnvEnum["statusMapping"] = "TIDEN_STATUS_MAPPING";
|
|
16
17
|
})(EnvEnum || (exports.EnvEnum = EnvEnum = {}));
|
|
17
18
|
/**
|
|
@@ -9,10 +9,16 @@ const writer_1 = require("../writer");
|
|
|
9
9
|
*/
|
|
10
10
|
const envToConfig = (env) => ({
|
|
11
11
|
mode: env[env_enum_1.EnvEnum.mode],
|
|
12
|
+
// TIDEN_FALLBACK was declared in EnvEnum, EnvType and the validation schema
|
|
13
|
+
// but never mapped here, so setting it did nothing: the value validated, then
|
|
14
|
+
// vanished. It decides what happens when `tiden` mode cannot start, which is
|
|
15
|
+
// exactly the silent-disable path the reporter now announces.
|
|
16
|
+
fallback: env[env_enum_1.EnvEnum.fallback],
|
|
12
17
|
debug: env[env_enum_1.EnvEnum.debug],
|
|
13
18
|
environment: env[env_enum_1.EnvEnum.environment],
|
|
14
19
|
captureLogs: env[env_enum_1.EnvEnum.captureLogs],
|
|
15
20
|
rootSuite: env[env_enum_1.EnvEnum.rootSuite],
|
|
21
|
+
rootDir: env[env_enum_1.EnvEnum.rootDir],
|
|
16
22
|
statusMapping: env[env_enum_1.EnvEnum.statusMapping] ?
|
|
17
23
|
Object.fromEntries(env[env_enum_1.EnvEnum.statusMapping].split(',').map(item => {
|
|
18
24
|
const [from, to] = item.split('=');
|
package/dist/env/env-type.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface EnvType {
|
|
|
8
8
|
[EnvEnum.environment]?: string;
|
|
9
9
|
[EnvEnum.captureLogs]?: boolean;
|
|
10
10
|
[EnvEnum.rootSuite]?: string;
|
|
11
|
+
[EnvEnum.rootDir]?: string;
|
|
11
12
|
[EnvEnum.statusMapping]?: string;
|
|
12
13
|
[EnvTidenEnum.product]?: string;
|
|
13
14
|
[EnvTidenEnum.uploadAttachments]?: boolean;
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -6,3 +6,7 @@ export type { FileSuiteNode } from './suite-file';
|
|
|
6
6
|
export { parseTidenIdsFromString } from './ids-parser';
|
|
7
7
|
export { filterPositiveIds } from './filter-positive-ids';
|
|
8
8
|
export { normalizeSuitePart } from './suite-normalizer';
|
|
9
|
+
export { normalizeSpecPath } from './spec-path';
|
|
10
|
+
export { resolveRootDir } from './spec-path';
|
|
11
|
+
export { resolveFilePath } from './spec-path';
|
|
12
|
+
export { isUsableFilePath } from './spec-path';
|
package/dist/internal/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeSuitePart = exports.filterPositiveIds = exports.parseTidenIdsFromString = exports.getFile = exports.extractAndCleanStep = exports.removeTidenIdsFromTitle = void 0;
|
|
3
|
+
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");
|
|
@@ -13,3 +13,11 @@ var filter_positive_ids_1 = require("./filter-positive-ids");
|
|
|
13
13
|
Object.defineProperty(exports, "filterPositiveIds", { enumerable: true, get: function () { return filter_positive_ids_1.filterPositiveIds; } });
|
|
14
14
|
var suite_normalizer_1 = require("./suite-normalizer");
|
|
15
15
|
Object.defineProperty(exports, "normalizeSuitePart", { enumerable: true, get: function () { return suite_normalizer_1.normalizeSuitePart; } });
|
|
16
|
+
var spec_path_1 = require("./spec-path");
|
|
17
|
+
Object.defineProperty(exports, "normalizeSpecPath", { enumerable: true, get: function () { return spec_path_1.normalizeSpecPath; } });
|
|
18
|
+
var spec_path_2 = require("./spec-path");
|
|
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; } });
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export declare function normalizeSpecPath(fullPath: string, cwd?: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* The base a spec-file segment is resolved against, in precedence order:
|
|
4
|
+
* an explicit `rootDir` option, then `TIDEN_ROOT_DIR`, then undefined
|
|
5
|
+
* (meaning `normalizeSpecPath` falls back to `process.cwd()`).
|
|
6
|
+
*
|
|
7
|
+
* Env is read here rather than through the usual config pipeline because that
|
|
8
|
+
* pipeline merges env inside `OptionsResolver`, whose result never reaches a
|
|
9
|
+
* framework reporter — so a framework-side setting would silently ignore its
|
|
10
|
+
* own environment variable.
|
|
11
|
+
*/
|
|
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;
|
|
@@ -0,0 +1,227 @@
|
|
|
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.normalizeSpecPath = normalizeSpecPath;
|
|
37
|
+
exports.resolveRootDir = resolveRootDir;
|
|
38
|
+
exports.resolveFilePath = resolveFilePath;
|
|
39
|
+
exports.isUsableFilePath = isUsableFilePath;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const env_enum_1 = require("../env/env-enum");
|
|
43
|
+
/**
|
|
44
|
+
* Normalizes an absolute spec-file path to a project-relative one, for use as
|
|
45
|
+
* the leading segment of a case signature.
|
|
46
|
+
*
|
|
47
|
+
* Kept as a SINGLE segment with its slashes intact — it is not split on `/`.
|
|
48
|
+
* That matches Playwright's `titlePath()` (which yields the spec file as one
|
|
49
|
+
* element) and the app-side vitest CI transform, so a case reported by the
|
|
50
|
+
* reporter and the same case reported by CI resolve to one identity.
|
|
51
|
+
*
|
|
52
|
+
* Path-shaped only: no filesystem access, and an id that does not live under
|
|
53
|
+
* `cwd` (a Vite virtual module, for instance) is returned unchanged rather
|
|
54
|
+
* than forced into a relative form.
|
|
55
|
+
*/
|
|
56
|
+
const SLASH = '/'.charCodeAt(0);
|
|
57
|
+
function normalizeSpecPath(fullPath, cwd = process.cwd()) {
|
|
58
|
+
const normalized = fullPath.replace(/\\/g, '/');
|
|
59
|
+
const root = rootPrefix(cwd);
|
|
60
|
+
return normalized.startsWith(root) ? normalized.slice(root.length) : normalized;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The root as a `/`-terminated prefix, with any trailing slashes collapsed to
|
|
64
|
+
* exactly one.
|
|
65
|
+
*
|
|
66
|
+
* Trimmed by scanning rather than with `/\/+$/`: that pattern backtracks
|
|
67
|
+
* quadratically on a root of many slashes, and the root can come from
|
|
68
|
+
* configuration (`rootDir` / `TIDEN_ROOT_DIR`). Same class of defect as the
|
|
69
|
+
* step-marker parser fixed in commons 0.1.1 — keep this regex-free.
|
|
70
|
+
*/
|
|
71
|
+
function rootPrefix(cwd) {
|
|
72
|
+
const normalized = cwd.replace(/\\/g, '/');
|
|
73
|
+
let end = normalized.length;
|
|
74
|
+
while (end > 0 && normalized.charCodeAt(end - 1) === SLASH) {
|
|
75
|
+
end -= 1;
|
|
76
|
+
}
|
|
77
|
+
return normalized.slice(0, end) + '/';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The base a spec-file segment is resolved against, in precedence order:
|
|
81
|
+
* an explicit `rootDir` option, then `TIDEN_ROOT_DIR`, then undefined
|
|
82
|
+
* (meaning `normalizeSpecPath` falls back to `process.cwd()`).
|
|
83
|
+
*
|
|
84
|
+
* Env is read here rather than through the usual config pipeline because that
|
|
85
|
+
* pipeline merges env inside `OptionsResolver`, whose result never reaches a
|
|
86
|
+
* framework reporter — so a framework-side setting would silently ignore its
|
|
87
|
+
* own environment variable.
|
|
88
|
+
*/
|
|
89
|
+
function resolveRootDir(explicit) {
|
|
90
|
+
const fromEnv = process.env[env_enum_1.EnvEnum.rootDir];
|
|
91
|
+
return explicit ?? (fromEnv !== undefined && fromEnv !== '' ? fromEnv : undefined);
|
|
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
|
+
}
|
|
@@ -25,6 +25,17 @@ export type OptionsType = {
|
|
|
25
25
|
debug?: boolean | undefined;
|
|
26
26
|
environment?: string | undefined;
|
|
27
27
|
rootSuite?: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Base directory the spec-file segment of a case signature is resolved
|
|
30
|
+
* against. Defaults to `process.cwd()`.
|
|
31
|
+
*
|
|
32
|
+
* Set it when the runner's working directory is not the root you want that
|
|
33
|
+
* segment measured from — a monorepo running vitest from a sub-package, for
|
|
34
|
+
* instance. The signature is matched byte-for-byte server-side, so every
|
|
35
|
+
* producer reporting into one product must agree on this base or the same
|
|
36
|
+
* test lands as two cases.
|
|
37
|
+
*/
|
|
38
|
+
rootDir?: string | undefined;
|
|
28
39
|
statusMapping?: Record<string, string> | undefined;
|
|
29
40
|
logging?: RecursivePartial<LoggingOptionsType> | undefined;
|
|
30
41
|
tiden?: RecursivePartial<TidenOptionsType> | undefined;
|
|
@@ -41,6 +41,16 @@ export declare class FallbackCoordinator {
|
|
|
41
41
|
* disabled or the operation produced no value.
|
|
42
42
|
*/
|
|
43
43
|
run<T>(op: (reporter: InternalReporterInterface) => Promise<T>, opName: string): Promise<T | undefined>;
|
|
44
|
+
/**
|
|
45
|
+
* Turning the reporter off MID-RUN, after an upstream (and fallback) failure.
|
|
46
|
+
*
|
|
47
|
+
* The failure itself is logged by the caller, but the consequence was not:
|
|
48
|
+
* the suite carried on, every later result went nowhere, and the run ended
|
|
49
|
+
* green. That is the same silent-failure symptom the construction-time
|
|
50
|
+
* announcements in `tiden.ts` were added to remove, reached by a different
|
|
51
|
+
* path — a token that is present but rejected, rather than one that is
|
|
52
|
+
* missing. Guarded by `this.disabled`, so it says it once.
|
|
53
|
+
*/
|
|
44
54
|
private disable;
|
|
45
55
|
private activateFallback;
|
|
46
56
|
private runOnFallback;
|
|
@@ -83,10 +83,22 @@ class FallbackCoordinator {
|
|
|
83
83
|
return this.runOnFallback(op, opName);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Turning the reporter off MID-RUN, after an upstream (and fallback) failure.
|
|
88
|
+
*
|
|
89
|
+
* The failure itself is logged by the caller, but the consequence was not:
|
|
90
|
+
* the suite carried on, every later result went nowhere, and the run ended
|
|
91
|
+
* green. That is the same silent-failure symptom the construction-time
|
|
92
|
+
* announcements in `tiden.ts` were added to remove, reached by a different
|
|
93
|
+
* path — a token that is present but rejected, rather than one that is
|
|
94
|
+
* missing. Guarded by `this.disabled`, so it says it once.
|
|
95
|
+
*/
|
|
86
96
|
disable() {
|
|
87
97
|
if (this.disabled)
|
|
88
98
|
return;
|
|
89
99
|
this.disabled = true;
|
|
100
|
+
this.logger.log('reporter disabled — the error above stopped it mid-run, and nothing further ' +
|
|
101
|
+
'from this suite will be reported to Tiden. Results already sent are unaffected.');
|
|
90
102
|
if (!this.onDisabledFired) {
|
|
91
103
|
this.onDisabledFired = true;
|
|
92
104
|
this.callbacks.onDisabled?.();
|
package/dist/tiden.d.ts
CHANGED
|
@@ -31,6 +31,17 @@ export declare class TidenReporter implements ReporterInterface {
|
|
|
31
31
|
*/
|
|
32
32
|
private constructor();
|
|
33
33
|
private buildLogger;
|
|
34
|
+
/**
|
|
35
|
+
* Announces that this reporter will send nothing, and why.
|
|
36
|
+
*
|
|
37
|
+
* A disabled reporter is indistinguishable from a working one: the suite
|
|
38
|
+
* runs, the tests pass, and nothing is reported. A field user concluded the
|
|
39
|
+
* reporter was broken and spent an hour on it before finding `mode: 'off'`
|
|
40
|
+
* in their own jest.config.js — the reporter was merely inert, and had said
|
|
41
|
+
* so nowhere. This is deliberately `log`, not `logDebug`: a message only
|
|
42
|
+
* visible once you already suspect the problem does not solve it.
|
|
43
|
+
*/
|
|
44
|
+
private logReporterDisabled;
|
|
34
45
|
private buildReporters;
|
|
35
46
|
private persistInitialState;
|
|
36
47
|
static getInstance(options: OptionsType): TidenReporter;
|
package/dist/tiden.js
CHANGED
|
@@ -87,6 +87,28 @@ class TidenReporter {
|
|
|
87
87
|
}
|
|
88
88
|
return new logger_1.Logger(opts);
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Announces that this reporter will send nothing, and why.
|
|
92
|
+
*
|
|
93
|
+
* A disabled reporter is indistinguishable from a working one: the suite
|
|
94
|
+
* runs, the tests pass, and nothing is reported. A field user concluded the
|
|
95
|
+
* reporter was broken and spent an hour on it before finding `mode: 'off'`
|
|
96
|
+
* in their own jest.config.js — the reporter was merely inert, and had said
|
|
97
|
+
* so nowhere. This is deliberately `log`, not `logDebug`: a message only
|
|
98
|
+
* visible once you already suspect the problem does not solve it.
|
|
99
|
+
*/
|
|
100
|
+
logReporterDisabled(reason, remedy, credentialsRelevant = false) {
|
|
101
|
+
// The credentials sentence is opt-in, not a suffix on every message. Telling
|
|
102
|
+
// someone who set `mode: "off"` on purpose that "tiden" mode needs an API
|
|
103
|
+
// token answers a question they did not ask, and padding every message with
|
|
104
|
+
// the same tail is how a message stops being read.
|
|
105
|
+
const credentials = credentialsRelevant
|
|
106
|
+
? ' "tiden" mode also requires tiden.api.token, tiden.product and' +
|
|
107
|
+
' tiden.api.baseUrl (TIDEN_API_TOKEN, TIDEN_PRODUCT_ID, TIDEN_BASE_URL).'
|
|
108
|
+
: '';
|
|
109
|
+
this.logger.log(`reporter disabled — nothing will be reported to Tiden, because ${reason}. ` +
|
|
110
|
+
`To enable it, ${remedy}.${credentials}`);
|
|
111
|
+
}
|
|
90
112
|
buildReporters(factory, resolved) {
|
|
91
113
|
let upstream;
|
|
92
114
|
let fallbackReporter;
|
|
@@ -98,11 +120,15 @@ class TidenReporter {
|
|
|
98
120
|
catch (error) {
|
|
99
121
|
if (error instanceof disabled_exception_1.DisabledException) {
|
|
100
122
|
disabled = true;
|
|
123
|
+
this.logReporterDisabled(`mode is "${resolved.effectiveMode}"`, 'set mode to "tiden" (or TIDEN_MODE=tiden) to report this run',
|
|
124
|
+
// Enabling it is the next thing they will do, and it needs all four.
|
|
125
|
+
true);
|
|
101
126
|
}
|
|
102
127
|
else {
|
|
103
128
|
this.logger.logError('Unable to create upstream reporter:', error);
|
|
104
129
|
if (resolved.composed.fallback === undefined) {
|
|
105
130
|
disabled = true;
|
|
131
|
+
this.logReporterDisabled('the upstream reporter could not be created and no fallback is configured', 'fix the error above');
|
|
106
132
|
return { upstream, fallback: fallbackReporter, disabled, useFallbackFromStart: false };
|
|
107
133
|
}
|
|
108
134
|
upstreamFailed = true;
|
|
@@ -113,13 +139,18 @@ class TidenReporter {
|
|
|
113
139
|
}
|
|
114
140
|
catch (error) {
|
|
115
141
|
if (error instanceof disabled_exception_1.DisabledException) {
|
|
116
|
-
if (upstreamFailed)
|
|
142
|
+
if (upstreamFailed) {
|
|
117
143
|
disabled = true;
|
|
144
|
+
this.logReporterDisabled('the upstream reporter could not be created (see the error above) and ' +
|
|
145
|
+
`fallback is "${resolved.effectiveFallback}"`, 'fix the error above, or set a fallback mode', true);
|
|
146
|
+
}
|
|
118
147
|
}
|
|
119
148
|
else {
|
|
120
149
|
this.logger.logError('Unable to create fallback reporter:', error);
|
|
121
|
-
if (upstreamFailed && upstream === undefined)
|
|
150
|
+
if (upstreamFailed && upstream === undefined) {
|
|
122
151
|
disabled = true;
|
|
152
|
+
this.logReporterDisabled('neither the upstream nor the fallback reporter could be created', 'fix the errors above');
|
|
153
|
+
}
|
|
123
154
|
}
|
|
124
155
|
}
|
|
125
156
|
const useFallbackFromStart = upstreamFailed && fallbackReporter !== undefined;
|