mapterrain 0.1.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/LICENSE +191 -0
- package/README.md +467 -0
- package/SECURITY.md +29 -0
- package/bin/postinstall.js +12 -0
- package/bin/terrain-cli.js +10 -0
- package/bin/terrain-installer.js +310 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding any notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
Copyright 2024-2026 PMcL
|
|
180
|
+
|
|
181
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
182
|
+
you may not use this file except in compliance with the License.
|
|
183
|
+
You may obtain a copy of the License at
|
|
184
|
+
|
|
185
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
186
|
+
|
|
187
|
+
Unless required by applicable law or agreed to in writing, software
|
|
188
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
189
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
190
|
+
See the License for the specific language governing permissions and
|
|
191
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# Terrain
|
|
2
|
+
|
|
3
|
+
**Map your test terrain.** Understand your test system in 30 seconds.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# Homebrew
|
|
7
|
+
brew install pmclSF/terrain/mapterrain
|
|
8
|
+
|
|
9
|
+
# npm
|
|
10
|
+
npm install -g mapterrain
|
|
11
|
+
|
|
12
|
+
cd your-repo
|
|
13
|
+
terrain analyze
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
That's it. No config, no setup, no test execution required.
|
|
17
|
+
|
|
18
|
+
> **New here?** Read the [Quickstart Guide](docs/quickstart.md) to understand your first report in 5 minutes.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
Terrain is a test system intelligence platform. It reads your repository — test code, source structure, coverage data, runtime artifacts, ownership files, and local policy — and builds a structural model of how your tests relate to your code. From that model it surfaces risk, quality gaps, redundancy, fragile dependencies, and migration readiness, all without running a single test.
|
|
23
|
+
|
|
24
|
+
The core idea: every codebase has a *test terrain* — the shape of its testing infrastructure, the density of coverage across areas, the hidden fault lines where a fixture change cascades into thousands of tests. Terrain makes that shape visible and navigable so you can make informed decisions about what to test, what to fix, and where to invest.
|
|
25
|
+
|
|
26
|
+
## What "Test Terrain" Means
|
|
27
|
+
|
|
28
|
+
Most teams know what tests they have. Few teams understand the *terrain* underneath:
|
|
29
|
+
|
|
30
|
+
- Which source files have no structural test coverage?
|
|
31
|
+
- Which shared fixtures fan out to thousands of tests, making any change to them a blast-radius problem?
|
|
32
|
+
- Which test clusters are near-duplicates burning CI time?
|
|
33
|
+
- Which areas have tests but weak assertions that wouldn't catch a real regression?
|
|
34
|
+
- When you change `auth/session.ts`, which 41 of your 18,000 tests actually matter?
|
|
35
|
+
|
|
36
|
+
Test terrain is the structural topology of your test system — the dependency graph, the coverage landscape, the duplication clusters, the fanout hotspots, the skip debt. Terrain maps it.
|
|
37
|
+
|
|
38
|
+
## Problems Terrain Solves
|
|
39
|
+
|
|
40
|
+
**"We don't know the state of our test system."** Teams inherit test suites they didn't write. Terrain gives a baseline in seconds: framework mix, coverage confidence, duplication, risk posture.
|
|
41
|
+
|
|
42
|
+
**"CI takes too long and we don't know what to cut."** Terrain identifies redundant tests, high-fanout fixtures, and confidence-based test selection — showing where CI time is wasted and what can be safely reduced.
|
|
43
|
+
|
|
44
|
+
**"We changed auth code — what tests should we worry about?"** Terrain traces your change through the import graph and structural dependencies, returning the impacted tests ranked by confidence, with reason chains explaining each selection.
|
|
45
|
+
|
|
46
|
+
**"A tool flagged something but won't explain why."** Every Terrain finding carries an evidence chain. `terrain explain` shows exactly what signals, dependency paths, and scoring rules produced each decision.
|
|
47
|
+
|
|
48
|
+
**"We're migrating frameworks and need to know what's blocking us."** Migration readiness, blockers by type, and preview-scoped difficulty assessment — all derived from static analysis.
|
|
49
|
+
|
|
50
|
+
## The Four Canonical Workflows
|
|
51
|
+
|
|
52
|
+
Terrain is organized around four questions. Everything else is a supporting view.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
terrain analyze "What is the state of our test system?"
|
|
56
|
+
terrain insights "What should we fix in our test system?"
|
|
57
|
+
terrain impact "What validations matter for this change?"
|
|
58
|
+
terrain explain "Why did Terrain make this decision?"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 1. Analyze — understand the test system
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
terrain analyze
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Terrain — Test Suite Analysis
|
|
69
|
+
============================================================
|
|
70
|
+
|
|
71
|
+
conftest.py fixture fans out to 3,100 tests — any change retriggers the frame/ suite.
|
|
72
|
+
|
|
73
|
+
Key Findings
|
|
74
|
+
------------------------------------------------------------
|
|
75
|
+
1. [HIGH] 23 source files (18%) have low structural coverage
|
|
76
|
+
2. [HIGH] 8 duplicate test clusters with 0.91+ similarity
|
|
77
|
+
3. [MEDIUM] 34 xfail markers older than 180 days
|
|
78
|
+
|
|
79
|
+
Repository Profile
|
|
80
|
+
------------------------------------------------------------
|
|
81
|
+
Test volume: very large
|
|
82
|
+
CI pressure: high
|
|
83
|
+
Coverage confidence: medium
|
|
84
|
+
Redundancy level: medium
|
|
85
|
+
Fanout burden: high
|
|
86
|
+
|
|
87
|
+
Validation Inventory
|
|
88
|
+
------------------------------------------------------------
|
|
89
|
+
Test files: 1,047
|
|
90
|
+
Test cases: 52,341
|
|
91
|
+
Frameworks:
|
|
92
|
+
pytest 1,047 files [unit]
|
|
93
|
+
|
|
94
|
+
Risk Posture
|
|
95
|
+
------------------------------------------------------------
|
|
96
|
+
health: MODERATE
|
|
97
|
+
coverage_depth: ELEVATED
|
|
98
|
+
coverage_diversity: STRONG
|
|
99
|
+
structural_risk: STRONG
|
|
100
|
+
operational_risk: STRONG
|
|
101
|
+
|
|
102
|
+
Next steps:
|
|
103
|
+
terrain insights prioritized actions and recommendations
|
|
104
|
+
terrain impact what tests matter for this change?
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 2. Insights — find what to improve
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
terrain insights
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
Terrain — Test System Health Report
|
|
115
|
+
============================================================
|
|
116
|
+
|
|
117
|
+
Health Grade: C
|
|
118
|
+
|
|
119
|
+
Reliability Problems (2)
|
|
120
|
+
[HIGH] 12 flaky tests with >10% failure rate
|
|
121
|
+
[MEDIUM] 34 skipped tests consuming CI resources
|
|
122
|
+
|
|
123
|
+
Coverage Debt (2)
|
|
124
|
+
[HIGH] 23 source files (18%) have low structural coverage
|
|
125
|
+
[MEDIUM] conftest.py fixture fans out to 3,100 tests
|
|
126
|
+
|
|
127
|
+
Recommended Actions
|
|
128
|
+
1. [reliability] Quarantine 12 flaky tests
|
|
129
|
+
why: Flaky tests block unrelated PRs and erode CI trust.
|
|
130
|
+
2. [coverage] Add tests for 23 uncovered source files
|
|
131
|
+
why: Changes in uncovered files cannot trigger test selection.
|
|
132
|
+
3. [optimization] Consolidate 8 duplicate test clusters
|
|
133
|
+
why: 0.91+ similarity — redundant CI cost with no coverage benefit.
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3. Impact — understand what a change affects
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
terrain impact --base main
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
Terrain Impact Analysis
|
|
144
|
+
============================================================
|
|
145
|
+
|
|
146
|
+
Summary: 3 file(s) changed, 41 test(s) relevant. Posture: needs_attention.
|
|
147
|
+
|
|
148
|
+
Impacted tests: 127 of 52,341 total
|
|
149
|
+
Coverage confidence: High
|
|
150
|
+
|
|
151
|
+
Recommended Tests (41)
|
|
152
|
+
------------------------------------------------------------
|
|
153
|
+
tests/groupby/test_groupby.py [exact]
|
|
154
|
+
Covers: groupby.py:GroupBy.aggregate
|
|
155
|
+
tests/groupby/test_apply.py [exact]
|
|
156
|
+
Covers: groupby.py:GroupBy.apply
|
|
157
|
+
tests/resample/test_base.py [inferred]
|
|
158
|
+
Reached via shared fixture path
|
|
159
|
+
...and 38 more
|
|
160
|
+
|
|
161
|
+
Protection Gaps
|
|
162
|
+
------------------------------------------------------------
|
|
163
|
+
[medium] pandas/core/groupby/ops.py — no covering tests found
|
|
164
|
+
|
|
165
|
+
Next steps:
|
|
166
|
+
terrain impact --show tests full test list
|
|
167
|
+
terrain impact --show gaps all protection gaps
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 4. Explain — understand why
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
terrain explain tests/io/json/test_pandas.py
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
Test File: tests/io/json/test_pandas.py
|
|
178
|
+
Framework: pytest
|
|
179
|
+
Tests: 84 Assertions: 312
|
|
180
|
+
|
|
181
|
+
Signals (3):
|
|
182
|
+
[high] networkDependency: 12 tests use @pytest.mark.network — flaky in CI
|
|
183
|
+
[medium] weakAssertion: 8 bare assert statements without descriptive messages
|
|
184
|
+
[low] xfailAccumulation: 3 xfail markers older than 180 days
|
|
185
|
+
|
|
186
|
+
Next steps:
|
|
187
|
+
terrain explain selection explain overall test selection strategy
|
|
188
|
+
terrain impact --show tests see all impacted tests
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
See [Canonical User Journeys](docs/product/canonical-user-journeys.md) for the full workflow specification and [example outputs](docs/examples/) for detailed report samples.
|
|
192
|
+
|
|
193
|
+
## Product Philosophy
|
|
194
|
+
|
|
195
|
+
**Inference first.** Terrain reads code. It parses imports, detects frameworks, resolves coverage relationships, and builds dependency graphs from what already exists in the repository. No annotations, no test tagging, no SDK integration required.
|
|
196
|
+
|
|
197
|
+
**Zero-config by default.** `terrain analyze` works on any repository with test files. Coverage data, runtime artifacts, ownership files, and policy rules are optional inputs that enrich the model — but the core analysis requires nothing beyond the code itself.
|
|
198
|
+
|
|
199
|
+
**Explainability over magic.** Every finding carries evidence: which signal type, what confidence level, what dependency path, what scoring rule. `terrain explain` exposes the full reasoning chain behind any decision. Teams should never wonder *why* Terrain said something.
|
|
200
|
+
|
|
201
|
+
**Conservative under uncertainty.** When Terrain encounters ambiguity — a dependency path with low confidence, a file that might or might not be a test — it flags the uncertainty rather than guessing. Impact analysis uses fallback policies with explicit confidence penalties rather than silently expanding scope.
|
|
202
|
+
|
|
203
|
+
**System health, not individual productivity.** Terrain measures the test system. It never attributes quality to individual developers. Ownership information is used for routing and triage, not scoring.
|
|
204
|
+
|
|
205
|
+
## Who Uses Terrain
|
|
206
|
+
|
|
207
|
+
Terrain is framework-agnostic and language-aware. The same analysis model applies across:
|
|
208
|
+
|
|
209
|
+
- **Frontend teams** — React/Vue component tests, Playwright/Cypress E2E suites, Vitest/Jest unit tests
|
|
210
|
+
- **Backend teams** — Go test suites, pytest collections, JUnit hierarchies, integration test infrastructure
|
|
211
|
+
- **Mobile teams** — cross-platform test suites with standard test frameworks
|
|
212
|
+
- **QA / SDET** — test portfolio management, coverage gap analysis, migration planning across frameworks
|
|
213
|
+
- **SRE / Platform** — CI optimization, test selection for pipelines, policy enforcement
|
|
214
|
+
- **AI / ML evaluation** — evaluation suite structure, benchmark test management, coverage across model behaviors
|
|
215
|
+
|
|
216
|
+
The structural model is the same. The signals and recommendations adapt to the framework and test patterns detected.
|
|
217
|
+
|
|
218
|
+
## How CI Optimization Emerges
|
|
219
|
+
|
|
220
|
+
Terrain does not start with CI optimization. It starts with understanding.
|
|
221
|
+
|
|
222
|
+
When you run `terrain analyze`, Terrain builds a structural model: which tests exist, which source files they cover, how they depend on shared fixtures, where duplication lives. From that model, CI optimization *emerges*:
|
|
223
|
+
|
|
224
|
+
- **Test selection** — `terrain impact` traces a diff through the dependency graph and returns only the tests that structurally matter, ranked by confidence. This is not a heuristic skip list — it is a graph traversal with evidence.
|
|
225
|
+
- **Redundancy reduction** — `terrain insights` surfaces duplicate test clusters. Removing or consolidating them directly reduces CI time without reducing coverage.
|
|
226
|
+
- **Fanout control** — High-fanout fixtures that trigger thousands of tests on any change are identified and prioritized for splitting.
|
|
227
|
+
- **Confidence-based runs** — Impact analysis assigns confidence scores. CI pipelines can run high-confidence tests immediately and defer low-confidence tests to nightly runs.
|
|
228
|
+
|
|
229
|
+
The result is faster CI that comes from *understanding the test system*, not from skipping tests and hoping for the best.
|
|
230
|
+
|
|
231
|
+
## Installation
|
|
232
|
+
|
|
233
|
+
### Homebrew (macOS and Linux)
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
brew install pmclSF/terrain/mapterrain
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
After the first install, you can also tap once and use the short formula name:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
brew tap pmclSF/terrain
|
|
243
|
+
brew install mapterrain
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### npm
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
npm install -g mapterrain
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Go install
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
go install github.com/pmclSF/terrain/cmd/terrain@latest
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Requires Go 1.23 or later.
|
|
259
|
+
|
|
260
|
+
### Pre-built binaries
|
|
261
|
+
|
|
262
|
+
Download the appropriate binary for your platform from [GitHub Releases](https://github.com/pmclSF/terrain/releases), then:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
chmod +x terrain
|
|
266
|
+
sudo mv terrain /usr/local/bin/
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Binaries are available for macOS, Linux, and Windows (amd64 and arm64).
|
|
270
|
+
|
|
271
|
+
### Build from source
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
git clone https://github.com/pmclSF/terrain.git
|
|
275
|
+
cd terrain
|
|
276
|
+
go build -o terrain ./cmd/terrain
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Verify installation
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
terrain --version
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Quick Start
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# Detect coverage/runtime data paths (recommended first step)
|
|
289
|
+
terrain init
|
|
290
|
+
|
|
291
|
+
# Analyze the current repository
|
|
292
|
+
terrain analyze
|
|
293
|
+
|
|
294
|
+
# JSON output for any command
|
|
295
|
+
terrain analyze --json
|
|
296
|
+
|
|
297
|
+
# See what a change affects
|
|
298
|
+
terrain impact --base main
|
|
299
|
+
|
|
300
|
+
# Get prioritized recommendations
|
|
301
|
+
terrain insights
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Commands
|
|
305
|
+
|
|
306
|
+
### Primary commands
|
|
307
|
+
|
|
308
|
+
| Command | Question |
|
|
309
|
+
|---------|----------|
|
|
310
|
+
| `terrain analyze` | What is the state of our test system? |
|
|
311
|
+
| `terrain insights` | What should we fix in our test system? |
|
|
312
|
+
| `terrain impact` | What validations matter for this change? |
|
|
313
|
+
| `terrain explain <target>` | Why did Terrain make this decision? |
|
|
314
|
+
|
|
315
|
+
### Supporting commands
|
|
316
|
+
|
|
317
|
+
| Command | Purpose |
|
|
318
|
+
|---------|---------|
|
|
319
|
+
| `terrain init` | Detect data files and print recommended analyze command |
|
|
320
|
+
| `terrain summary` | Executive summary with risk, trends, benchmark readiness |
|
|
321
|
+
| `terrain focus` | Prioritized next actions |
|
|
322
|
+
| `terrain posture` | Detailed posture breakdown with measurement evidence |
|
|
323
|
+
| `terrain portfolio` | Portfolio intelligence: cost, breadth, leverage, redundancy |
|
|
324
|
+
| `terrain metrics` | Aggregate metrics scorecard |
|
|
325
|
+
| `terrain compare` | Compare two snapshots for trend tracking |
|
|
326
|
+
| `terrain select-tests` | Recommend protective test set for a change |
|
|
327
|
+
| `terrain pr` | PR/change-scoped analysis |
|
|
328
|
+
| `terrain show <type> <id>` | Drill into test, unit, owner, or finding |
|
|
329
|
+
| `terrain migration <sub>` | Migration readiness, blockers, or preview |
|
|
330
|
+
| `terrain policy check` | Evaluate local policy rules |
|
|
331
|
+
| `terrain export benchmark` | Privacy-safe JSON export for benchmarking |
|
|
332
|
+
| `terrain serve` | Local HTTP server with HTML report and JSON API |
|
|
333
|
+
|
|
334
|
+
### AI / eval
|
|
335
|
+
|
|
336
|
+
| Command | Purpose |
|
|
337
|
+
|---------|---------|
|
|
338
|
+
| `terrain ai list` | List detected scenarios, prompts, datasets, eval files |
|
|
339
|
+
| `terrain ai doctor` | Validate AI/eval setup and surface configuration issues |
|
|
340
|
+
| `terrain ai run` | Execute eval scenarios and collect results |
|
|
341
|
+
| `terrain ai replay` | Replay and verify a previous eval run artifact |
|
|
342
|
+
| `terrain ai record` | Record eval results as a baseline snapshot |
|
|
343
|
+
| `terrain ai baseline` | Manage eval baselines (show, compare) |
|
|
344
|
+
|
|
345
|
+
### Conversion / migration
|
|
346
|
+
|
|
347
|
+
| Command | Purpose |
|
|
348
|
+
|---------|---------|
|
|
349
|
+
| `terrain convert <source>` | Go-native test conversion (25 directions) |
|
|
350
|
+
| `terrain convert-config <source>` | Convert framework config files |
|
|
351
|
+
| `terrain migrate <dir>` | Project-wide migration with state tracking |
|
|
352
|
+
| `terrain estimate <dir>` | Estimate migration complexity |
|
|
353
|
+
| `terrain status` | Show migration progress |
|
|
354
|
+
| `terrain checklist` | Generate migration checklist |
|
|
355
|
+
| `terrain doctor [path]` | Run migration diagnostics |
|
|
356
|
+
| `terrain reset` | Clear migration state |
|
|
357
|
+
| `terrain list-conversions` | List supported conversion directions |
|
|
358
|
+
| `terrain shorthands` | List shorthand aliases (e.g., `cy2pw`, `jest2vt`) |
|
|
359
|
+
| `terrain detect <file-or-dir>` | Detect dominant framework |
|
|
360
|
+
|
|
361
|
+
### Advanced / debug
|
|
362
|
+
|
|
363
|
+
| Command | Purpose |
|
|
364
|
+
|---------|---------|
|
|
365
|
+
| `terrain debug graph` | Dependency graph statistics |
|
|
366
|
+
| `terrain debug coverage` | Structural coverage analysis |
|
|
367
|
+
| `terrain debug fanout` | High-fanout node analysis |
|
|
368
|
+
| `terrain debug duplicates` | Duplicate test cluster analysis |
|
|
369
|
+
| `terrain debug depgraph` | Full dependency graph analysis (all engines) |
|
|
370
|
+
|
|
371
|
+
Repository-scoped commands support `--root PATH`, and machine-readable commands support `--json`. Most analysis commands support `--verbose` for additional detail. Run `terrain <command> --help` for full flag documentation.
|
|
372
|
+
|
|
373
|
+
## Architecture Overview
|
|
374
|
+
|
|
375
|
+
Terrain is built around a signal-first architecture:
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
Repository scan → Signal detection → Risk modeling → Reporting
|
|
379
|
+
│ │ │ │
|
|
380
|
+
test files framework-specific explainable human-readable
|
|
381
|
+
source files pattern detectors risk scoring + JSON output
|
|
382
|
+
coverage data quality signals with evidence
|
|
383
|
+
runtime artifacts health signals chains
|
|
384
|
+
ownership files migration signals
|
|
385
|
+
policy rules governance signals
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- **Signals** are the core abstraction — every finding is a structured signal with type, severity, confidence, evidence, and location
|
|
389
|
+
- **Snapshots** (`TestSuiteSnapshot`) are the canonical serialized artifact — the complete structural model of a test system at a point in time
|
|
390
|
+
- **Risk surfaces** are derived from signals with explainable scoring across dimensions (quality, reliability, speed, governance)
|
|
391
|
+
- **Dependency graphs** model import relationships, fixture fanout, and structural coverage
|
|
392
|
+
- **Reports** synthesize signals, risk, trends, and benchmark readiness into actionable output
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
cmd/terrain/ CLI (30+ commands)
|
|
396
|
+
internal/ 47 Go packages covering analysis, signals, risk,
|
|
397
|
+
impact, depgraph, measurement, reporting, and more
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
See [DESIGN.md](DESIGN.md) for the full architecture overview and package map, [docs/architecture/](docs/architecture/) for detailed design documents, and [docs/json-schema.md](docs/json-schema.md) for JSON output structure.
|
|
401
|
+
|
|
402
|
+
## Snapshot Workflow
|
|
403
|
+
|
|
404
|
+
Terrain supports local snapshot history for trend tracking:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
# Save a snapshot
|
|
408
|
+
terrain analyze --write-snapshot
|
|
409
|
+
|
|
410
|
+
# Later, save another snapshot
|
|
411
|
+
terrain analyze --write-snapshot
|
|
412
|
+
|
|
413
|
+
# Compare the two most recent snapshots
|
|
414
|
+
terrain compare
|
|
415
|
+
|
|
416
|
+
# Executive summary automatically includes trend highlights
|
|
417
|
+
terrain summary
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Snapshots are stored in `.terrain/snapshots/` as timestamped JSON files.
|
|
421
|
+
|
|
422
|
+
## Policy
|
|
423
|
+
|
|
424
|
+
Define local policy rules in `.terrain/policy.yaml`:
|
|
425
|
+
|
|
426
|
+
```yaml
|
|
427
|
+
rules:
|
|
428
|
+
disallow_skipped_tests: true
|
|
429
|
+
max_weak_assertions: 10
|
|
430
|
+
max_mock_heavy_tests: 5
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Then check compliance:
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
terrain policy check # human-readable output
|
|
437
|
+
terrain policy check --json # JSON output for CI
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Exit code 0 = pass, 2 = violations found, 1 = error.
|
|
441
|
+
|
|
442
|
+
## Documentation
|
|
443
|
+
|
|
444
|
+
- [Quickstart Guide](docs/quickstart.md) — understand your first report in 5 minutes
|
|
445
|
+
- [CLI Specification](docs/cli-spec.md) — full command and flag reference
|
|
446
|
+
- [Example Reports](docs/examples/) — analyze, impact, insights, explain output samples
|
|
447
|
+
- [Canonical User Journeys](docs/product/canonical-user-journeys.md) — primary workflows and expected outcomes
|
|
448
|
+
- [Signal Model](docs/signal-model.md) — the core signal abstraction
|
|
449
|
+
- [Architecture](docs/architecture/) — design documents and technical specifications
|
|
450
|
+
- [Contributing](CONTRIBUTING.md) — how to build, test, and extend Terrain
|
|
451
|
+
|
|
452
|
+
## Development
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
# Build
|
|
456
|
+
go build -o terrain ./cmd/terrain
|
|
457
|
+
|
|
458
|
+
# Test
|
|
459
|
+
go test ./cmd/... ./internal/...
|
|
460
|
+
|
|
461
|
+
# Full release verification
|
|
462
|
+
make release-verify
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## License
|
|
466
|
+
|
|
467
|
+
Apache License 2.0 — see [LICENSE](LICENSE) for details.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Security updates are provided for the latest published major version of Terrain, covering both the Go CLI (`terrain`) and the JavaScript converter package.
|
|
6
|
+
|
|
7
|
+
## Reporting a Vulnerability
|
|
8
|
+
|
|
9
|
+
If you believe you found a security issue, report it privately:
|
|
10
|
+
|
|
11
|
+
- Open a private GitHub security advisory if available for this repository.
|
|
12
|
+
- If private advisories are unavailable, open a regular issue without exploit details and request a secure contact channel.
|
|
13
|
+
|
|
14
|
+
Please include:
|
|
15
|
+
|
|
16
|
+
- A clear description of the issue and impact
|
|
17
|
+
- Reproduction steps or proof-of-concept input
|
|
18
|
+
- Affected version(s)
|
|
19
|
+
- Any suggested mitigation
|
|
20
|
+
|
|
21
|
+
## Response Expectations
|
|
22
|
+
|
|
23
|
+
- We aim to acknowledge reports within 3 business days.
|
|
24
|
+
- We aim to provide an initial triage within 7 business days.
|
|
25
|
+
- Confirmed vulnerabilities will be patched and released as quickly as possible.
|
|
26
|
+
|
|
27
|
+
## Disclosure
|
|
28
|
+
|
|
29
|
+
Please avoid public disclosure until a fix is available and maintainers confirm coordinated disclosure timing.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { ensureTerrainBinary } from './terrain-installer.js';
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
await ensureTerrainBinary({ quiet: false });
|
|
7
|
+
} catch (error) {
|
|
8
|
+
process.stderr.write(
|
|
9
|
+
`[mapterrain] Warning: ${error.message}\n` +
|
|
10
|
+
'[mapterrain] The `terrain` command will try again on first run.\n'
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync, spawn } from 'child_process';
|
|
4
|
+
import { createWriteStream, existsSync } from 'fs';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import https from 'https';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { pipeline } from 'stream/promises';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
14
|
+
const packageJson = JSON.parse(
|
|
15
|
+
await fs.readFile(path.join(packageRoot, 'package.json'), 'utf8')
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const GITHUB_OWNER = 'pmclSF';
|
|
19
|
+
const GITHUB_REPO = 'terrain';
|
|
20
|
+
|
|
21
|
+
function currentTarget() {
|
|
22
|
+
const goosMap = {
|
|
23
|
+
darwin: 'darwin',
|
|
24
|
+
linux: 'linux',
|
|
25
|
+
win32: 'windows',
|
|
26
|
+
};
|
|
27
|
+
const goarchMap = {
|
|
28
|
+
x64: 'amd64',
|
|
29
|
+
arm64: 'arm64',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const goos = goosMap[process.platform];
|
|
33
|
+
const goarch = goarchMap[process.arch];
|
|
34
|
+
if (!goos || !goarch) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Unsupported platform ${process.platform}/${process.arch}. ` +
|
|
37
|
+
'Install Terrain manually from GitHub Releases or via Homebrew.'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
goos,
|
|
43
|
+
goarch,
|
|
44
|
+
archiveExt: goos === 'windows' ? 'zip' : 'tar.gz',
|
|
45
|
+
binaryName: goos === 'windows' ? 'terrain.exe' : 'terrain',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isDevelopmentCheckout(rootDir = packageRoot) {
|
|
50
|
+
return existsSync(path.join(rootDir, 'cmd', 'terrain'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function installedBinaryPath(rootDir = packageRoot) {
|
|
54
|
+
const target = currentTarget();
|
|
55
|
+
return path.join(
|
|
56
|
+
rootDir,
|
|
57
|
+
'vendor',
|
|
58
|
+
'terrain',
|
|
59
|
+
`${target.goos}-${target.goarch}`,
|
|
60
|
+
target.binaryName
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function localBuiltBinaryPath(rootDir = packageRoot) {
|
|
65
|
+
const target = currentTarget();
|
|
66
|
+
return path.join(rootDir, target.binaryName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function archiveFileName(version) {
|
|
70
|
+
const target = currentTarget();
|
|
71
|
+
return `terrain_${version}_${target.goos}_${target.goarch}.${target.archiveExt}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function archiveDownloadUrl(version) {
|
|
75
|
+
const baseUrl =
|
|
76
|
+
process.env.TERRAIN_INSTALLER_BASE_URL ||
|
|
77
|
+
`https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download`;
|
|
78
|
+
return `${baseUrl}/v${version}/${archiveFileName(version)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function ensureDirectory(dir) {
|
|
82
|
+
await fs.mkdir(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function copyBinary(sourcePath, destinationPath) {
|
|
86
|
+
await ensureDirectory(path.dirname(destinationPath));
|
|
87
|
+
await fs.copyFile(sourcePath, destinationPath);
|
|
88
|
+
if (process.platform !== 'win32') {
|
|
89
|
+
await fs.chmod(destinationPath, 0o755);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function log(message, quiet = false) {
|
|
94
|
+
if (!quiet) {
|
|
95
|
+
process.stderr.write(`${message}\n`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function downloadFile(url, destinationPath) {
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
const request = https.get(
|
|
102
|
+
url,
|
|
103
|
+
{
|
|
104
|
+
headers: {
|
|
105
|
+
'User-Agent': `${packageJson.name}/${packageJson.version}`,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
async (response) => {
|
|
109
|
+
if (
|
|
110
|
+
response.statusCode &&
|
|
111
|
+
response.statusCode >= 300 &&
|
|
112
|
+
response.statusCode < 400 &&
|
|
113
|
+
response.headers.location
|
|
114
|
+
) {
|
|
115
|
+
response.resume();
|
|
116
|
+
try {
|
|
117
|
+
await downloadFile(response.headers.location, destinationPath);
|
|
118
|
+
resolve();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
reject(error);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (response.statusCode !== 200) {
|
|
126
|
+
response.resume();
|
|
127
|
+
reject(
|
|
128
|
+
new Error(
|
|
129
|
+
`download failed with HTTP ${response.statusCode} for ${url}`
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await pipeline(response, createWriteStream(destinationPath));
|
|
137
|
+
resolve();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
reject(error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
request.on('error', reject);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function extractArchive(archivePath, extractDir) {
|
|
149
|
+
if (archivePath.endsWith('.tar.gz')) {
|
|
150
|
+
execFileSync('tar', ['-xzf', archivePath, '-C', extractDir], {
|
|
151
|
+
stdio: 'pipe',
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
execFileSync('tar', ['-xf', archivePath, '-C', extractDir], {
|
|
158
|
+
stdio: 'pipe',
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (process.platform !== 'win32') {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
execFileSync(
|
|
165
|
+
'powershell.exe',
|
|
166
|
+
[
|
|
167
|
+
'-NoLogo',
|
|
168
|
+
'-NoProfile',
|
|
169
|
+
'-Command',
|
|
170
|
+
`Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${extractDir}' -Force`,
|
|
171
|
+
],
|
|
172
|
+
{ stdio: 'pipe' }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function findBinary(dir, binaryName) {
|
|
178
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
const fullPath = path.join(dir, entry.name);
|
|
181
|
+
if (entry.isFile() && entry.name === binaryName) {
|
|
182
|
+
return fullPath;
|
|
183
|
+
}
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
const nested = await findBinary(fullPath, binaryName);
|
|
186
|
+
if (nested) {
|
|
187
|
+
return nested;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function ensureTerrainBinary({
|
|
195
|
+
rootDir = packageRoot,
|
|
196
|
+
quiet = false,
|
|
197
|
+
version = packageJson.version,
|
|
198
|
+
env = process.env,
|
|
199
|
+
} = {}) {
|
|
200
|
+
const binaryPath = installedBinaryPath(rootDir);
|
|
201
|
+
if (existsSync(binaryPath)) {
|
|
202
|
+
return binaryPath;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const localOverride = env.TERRAIN_INSTALLER_LOCAL_BINARY;
|
|
206
|
+
if (localOverride && existsSync(localOverride)) {
|
|
207
|
+
log(`Using local Terrain binary override: ${localOverride}`, quiet);
|
|
208
|
+
await copyBinary(localOverride, binaryPath);
|
|
209
|
+
return binaryPath;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isDevelopmentCheckout(rootDir)) {
|
|
213
|
+
const localBinary = localBuiltBinaryPath(rootDir);
|
|
214
|
+
if (existsSync(localBinary)) {
|
|
215
|
+
return localBinary;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (env.TERRAIN_INSTALLER_SKIP_DOWNLOAD === '1') {
|
|
221
|
+
throw new Error(
|
|
222
|
+
'Terrain binary download skipped because TERRAIN_INSTALLER_SKIP_DOWNLOAD=1.'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'terrain-install-'));
|
|
227
|
+
const archivePath = path.join(tempDir, archiveFileName(version));
|
|
228
|
+
const extractDir = path.join(tempDir, 'extract');
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
log(
|
|
232
|
+
`Downloading Terrain ${version} for ${process.platform}/${process.arch}...`,
|
|
233
|
+
quiet
|
|
234
|
+
);
|
|
235
|
+
await downloadFile(archiveDownloadUrl(version), archivePath);
|
|
236
|
+
await ensureDirectory(extractDir);
|
|
237
|
+
extractArchive(archivePath, extractDir);
|
|
238
|
+
|
|
239
|
+
const extractedBinary = await findBinary(
|
|
240
|
+
extractDir,
|
|
241
|
+
currentTarget().binaryName
|
|
242
|
+
);
|
|
243
|
+
if (!extractedBinary) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`downloaded archive ${path.basename(archivePath)} did not contain ${currentTarget().binaryName}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await copyBinary(extractedBinary, binaryPath);
|
|
250
|
+
log(`Installed Terrain binary to ${binaryPath}`, quiet);
|
|
251
|
+
return binaryPath;
|
|
252
|
+
} finally {
|
|
253
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function runTerrainCli(argv = process.argv.slice(2)) {
|
|
258
|
+
const rootDir = packageRoot;
|
|
259
|
+
|
|
260
|
+
if (isDevelopmentCheckout(rootDir)) {
|
|
261
|
+
const localBinary = localBuiltBinaryPath(rootDir);
|
|
262
|
+
if (existsSync(localBinary)) {
|
|
263
|
+
await runBinary(localBinary, argv);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await runBinary('go', ['run', './cmd/terrain', ...argv], rootDir);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let binaryPath;
|
|
272
|
+
try {
|
|
273
|
+
binaryPath = await ensureTerrainBinary({ rootDir });
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`${error.message}\n\n` +
|
|
277
|
+
'Fallback install options:\n' +
|
|
278
|
+
' brew install pmclSF/terrain/mapterrain\n' +
|
|
279
|
+
' go install github.com/pmclSF/terrain/cmd/terrain@latest'
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!binaryPath) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
'No Terrain binary is available in this checkout yet. ' +
|
|
286
|
+
'Build it with `go build -o terrain ./cmd/terrain` or run the Go CLI directly.'
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
await runBinary(binaryPath, argv);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function runBinary(command, args, cwd = undefined) {
|
|
294
|
+
await new Promise((resolve, reject) => {
|
|
295
|
+
const child = spawn(command, args, {
|
|
296
|
+
cwd,
|
|
297
|
+
stdio: 'inherit',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
child.on('error', reject);
|
|
301
|
+
child.on('exit', (code, signal) => {
|
|
302
|
+
if (signal) {
|
|
303
|
+
process.kill(process.pid, signal);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
process.exitCode = code ?? 1;
|
|
307
|
+
resolve();
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mapterrain",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terrain test intelligence CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mapterrain": "bin/terrain-cli.js",
|
|
8
|
+
"terrain": "bin/terrain-cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"README.md",
|
|
13
|
+
"SECURITY.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/pmclSF/terrain.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/pmclSF/terrain#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/pmclSF/terrain/issues"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"postinstall": "node bin/postinstall.js",
|
|
26
|
+
"test": "node scripts/verify-pack.js",
|
|
27
|
+
"lint": "eslint \"bin/*.js\" \"scripts/*.js\"",
|
|
28
|
+
"format": "prettier --write \"bin/*.js\" \"scripts/*.js\"",
|
|
29
|
+
"format:check": "prettier --check \"bin/*.js\" \"scripts/*.js\"",
|
|
30
|
+
"start": "node bin/terrain-cli.js",
|
|
31
|
+
"convert": "node bin/terrain-cli.js convert",
|
|
32
|
+
"lint-staged": "lint-staged",
|
|
33
|
+
"release:verify": "npm run format:check && npm run lint && npm test",
|
|
34
|
+
"prepublishOnly": "npm run format:check && npm run lint && npm test"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"terrain",
|
|
38
|
+
"test-intelligence",
|
|
39
|
+
"test-analysis",
|
|
40
|
+
"risk-analysis",
|
|
41
|
+
"playwright",
|
|
42
|
+
"cypress",
|
|
43
|
+
"selenium",
|
|
44
|
+
"jest",
|
|
45
|
+
"vitest",
|
|
46
|
+
"mocha",
|
|
47
|
+
"jasmine",
|
|
48
|
+
"junit",
|
|
49
|
+
"testng",
|
|
50
|
+
"pytest",
|
|
51
|
+
"unittest",
|
|
52
|
+
"nose2",
|
|
53
|
+
"webdriverio",
|
|
54
|
+
"puppeteer",
|
|
55
|
+
"testcafe",
|
|
56
|
+
"testing",
|
|
57
|
+
"automation",
|
|
58
|
+
"migration",
|
|
59
|
+
"e2e-testing",
|
|
60
|
+
"test-migration",
|
|
61
|
+
"test-converter"
|
|
62
|
+
],
|
|
63
|
+
"author": "pmclSF",
|
|
64
|
+
"license": "Apache-2.0",
|
|
65
|
+
"dependencies": {},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@commitlint/cli": "^19.6.1",
|
|
68
|
+
"@commitlint/config-conventional": "^19.6.0",
|
|
69
|
+
"eslint": "^8.57.1",
|
|
70
|
+
"eslint-config-prettier": "^10.1.8",
|
|
71
|
+
"lint-staged": "^16.2.7",
|
|
72
|
+
"prettier": "^3.0.0"
|
|
73
|
+
},
|
|
74
|
+
"lint-staged": {
|
|
75
|
+
"{bin,scripts}/*.js": [
|
|
76
|
+
"prettier --write",
|
|
77
|
+
"eslint --fix --max-warnings=0"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"engines": {
|
|
81
|
+
"node": ">=22.0.0"
|
|
82
|
+
},
|
|
83
|
+
"sideEffects": false
|
|
84
|
+
}
|