@sasjs/cli 6.2.7 → 6.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/README.md +1 -0
- package/build/commands/auth/authCommand.js +5 -3
- package/build/commands/auth/login.d.ts +9 -3
- package/build/commands/auth/login.js +40 -21
- package/build/commands/auth/spec/login.spec.js +103 -3
- package/build/commands/compile/internal/compileTestFile.d.ts +2 -0
- package/build/commands/compile/internal/compileTestFile.js +48 -60
- package/build/commands/compile/internal/spec/compileTestFile.spec.js +158 -0
- package/build/commands/help/help.js +1 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/testing.d.ts +33 -0
- package/build/types/testing.js +1 -0
- package/build/utils/auth.d.ts +30 -0
- package/build/utils/auth.js +230 -0
- package/build/utils/config.js +2 -2
- package/build/utils/spec/sasjsLogin.spec.d.ts +1 -0
- package/build/utils/spec/sasjsLogin.spec.js +370 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- Scaffold a new SAS App in seconds (`sasjs create myApp -t jobs`)
|
|
18
18
|
- Compile / Build SAS Jobs, Services & Macros _locally_ and deploy / execute _remotely_, in isolation from other developers (`sasjs cbd`)
|
|
19
19
|
- Compile a single Job, Service or Test without touching the rest of the build (`sasjs compile identify --source sasjs/jobs/daily/load.sas`)
|
|
20
|
+
- Write the test coverage report to `sasjsbuild/coverage.json` on every compile, ready for a CI job to publish
|
|
20
21
|
- No dependency on the server filesystem
|
|
21
22
|
- Optionally synchronise a local folder onto the server filesystem using `sasjs fs`
|
|
22
23
|
- Trigger Viya Jobs & Flows (`sasjs job execute` / `sasjs flow execute`)
|
|
@@ -68,15 +68,17 @@ var syntax = 'auth [subCommand] [options]';
|
|
|
68
68
|
var usage = 'sasjs auth login --target <target-name> | sasjs auth [options]';
|
|
69
69
|
var description = "Authenticates against the specified target.\n" +
|
|
70
70
|
"With the 'login' subCommand, authentication is performed with a SAS username and password " +
|
|
71
|
-
"(no OAuth client/secret required
|
|
71
|
+
"(no OAuth client/secret required) and the resulting tokens are saved. " +
|
|
72
|
+
"Supported on SASVIYA targets, and on SASJS targets, where the server's own local login is used.\n" +
|
|
72
73
|
"Without a subCommand, behaves like 'sasjs add cred' (client/secret based authentication).\n" +
|
|
73
74
|
"\n" +
|
|
74
75
|
"Non-interactive use (CI/agents): set SAS_USERNAME and SAS_PASSWORD environment variables, " +
|
|
75
|
-
"or pipe the password and use --password-stdin."
|
|
76
|
+
"or pipe the password and use --password-stdin. On a SASJS target, set SASJS_CLIENT to the " +
|
|
77
|
+
"registered client id when the account is not an administrator.";
|
|
76
78
|
var examples = [
|
|
77
79
|
{
|
|
78
80
|
command: 'sasjs auth login --target <target-name>',
|
|
79
|
-
description: 'Logs in with SAS username/password (no client/secret required).'
|
|
81
|
+
description: 'Logs in with SAS username/password (no client/secret required) on a SASVIYA or SASJS target.'
|
|
80
82
|
},
|
|
81
83
|
{
|
|
82
84
|
command: 'sasjs auth login -t <target-name>',
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { Target } from '@sasjs/utils';
|
|
2
2
|
/**
|
|
3
|
-
* Authenticates against a SASVIYA target using a SAS username and
|
|
4
|
-
*
|
|
5
|
-
* `sas.cli` client) and persists the resulting token pair. This enables all
|
|
3
|
+
* Authenticates against a SASVIYA or SASJS target using a SAS username and
|
|
4
|
+
* password, and persists the resulting token pair. This enables all
|
|
6
5
|
* authenticated commands (`sasjs run`, `sasjs deploy`, etc.) on estates where
|
|
7
6
|
* no administrator-registered OAuth client/secret is available.
|
|
8
7
|
*
|
|
8
|
+
* The two server types issue tokens differently: SASVIYA uses the OAuth2
|
|
9
|
+
* resource owner password grant against the built-in, secret-less `sas.cli`
|
|
10
|
+
* client, while SASJS has no password grant and is authenticated through the
|
|
11
|
+
* server's own local login (@see getTokensWithSasjsLogin). Everything else -
|
|
12
|
+
* how the credentials are obtained, and the rule that nothing is prompted for
|
|
13
|
+
* when there is no TTY - is shared by both.
|
|
14
|
+
*
|
|
9
15
|
* Credentials are resolved in this order (highest precedence first):
|
|
10
16
|
*
|
|
11
17
|
* Username: SAS_USERNAME env var > interactive prompt (TTY only)
|
|
@@ -139,12 +139,18 @@ var readPasswordFromStdin = function () { return __awaiter(void 0, void 0, void
|
|
|
139
139
|
});
|
|
140
140
|
}); };
|
|
141
141
|
/**
|
|
142
|
-
* Authenticates against a SASVIYA target using a SAS username and
|
|
143
|
-
*
|
|
144
|
-
* `sas.cli` client) and persists the resulting token pair. This enables all
|
|
142
|
+
* Authenticates against a SASVIYA or SASJS target using a SAS username and
|
|
143
|
+
* password, and persists the resulting token pair. This enables all
|
|
145
144
|
* authenticated commands (`sasjs run`, `sasjs deploy`, etc.) on estates where
|
|
146
145
|
* no administrator-registered OAuth client/secret is available.
|
|
147
146
|
*
|
|
147
|
+
* The two server types issue tokens differently: SASVIYA uses the OAuth2
|
|
148
|
+
* resource owner password grant against the built-in, secret-less `sas.cli`
|
|
149
|
+
* client, while SASJS has no password grant and is authenticated through the
|
|
150
|
+
* server's own local login (@see getTokensWithSasjsLogin). Everything else -
|
|
151
|
+
* how the credentials are obtained, and the rule that nothing is prompted for
|
|
152
|
+
* when there is no TTY - is shared by both.
|
|
153
|
+
*
|
|
148
154
|
* Credentials are resolved in this order (highest precedence first):
|
|
149
155
|
*
|
|
150
156
|
* Username: SAS_USERNAME env var > interactive prompt (TTY only)
|
|
@@ -175,15 +181,16 @@ var authLogin = function (target_1) {
|
|
|
175
181
|
args_1[_i - 1] = arguments[_i];
|
|
176
182
|
}
|
|
177
183
|
return __awaiter(void 0, __spreadArray([target_1], __read(args_1), false), void 0, function (target, insecure, passwordStdin) {
|
|
178
|
-
var isTty, envUser, envPass, user, hint, pass, promptedPass, _a, access_token, refresh_token,
|
|
179
|
-
var
|
|
184
|
+
var isTty, envUser, envPass, user, hint, pass, promptedPass, _a, access_token_1, refresh_token_1, client, displayName, _b, access_token, refresh_token, _c, id, name;
|
|
185
|
+
var _d, _e, _f, _g, _h;
|
|
180
186
|
if (insecure === void 0) { insecure = false; }
|
|
181
187
|
if (passwordStdin === void 0) { passwordStdin = false; }
|
|
182
|
-
return __generator(this, function (
|
|
183
|
-
switch (
|
|
188
|
+
return __generator(this, function (_j) {
|
|
189
|
+
switch (_j.label) {
|
|
184
190
|
case 0:
|
|
185
|
-
if (target.serverType !== utils_1.ServerType.SasViya
|
|
186
|
-
|
|
191
|
+
if (target.serverType !== utils_1.ServerType.SasViya &&
|
|
192
|
+
target.serverType !== utils_1.ServerType.Sasjs) {
|
|
193
|
+
throw new Error("'sasjs auth login' is supported for SASVIYA and SASJS targets. " +
|
|
187
194
|
"Target '".concat(target.name, "' is of type '").concat(target.serverType, "'. ") +
|
|
188
195
|
"Use 'sasjs add cred -t ".concat(target.name, "' instead."));
|
|
189
196
|
}
|
|
@@ -192,7 +199,7 @@ var authLogin = function (target_1) {
|
|
|
192
199
|
}
|
|
193
200
|
if (insecure) {
|
|
194
201
|
target = new utils_1.Target(__assign(__assign({}, target.toJson(false)), { httpsAgentOptions: __assign(__assign({}, target.httpsAgentOptions), { allowInsecureRequests: true, rejectUnauthorized: false }) }));
|
|
195
|
-
(
|
|
202
|
+
(_d = process.logger) === null || _d === void 0 ? void 0 : _d.warn('Executing with insecure connection.');
|
|
196
203
|
}
|
|
197
204
|
isTty = !!process.stdin.isTTY;
|
|
198
205
|
envUser = process.env.SAS_USERNAME;
|
|
@@ -204,7 +211,7 @@ var authLogin = function (target_1) {
|
|
|
204
211
|
if (!(isTty && !passwordStdin)) return [3 /*break*/, 3];
|
|
205
212
|
return [4 /*yield*/, (0, utils_1.getString)('Please enter your SAS username', function (v) { return !!v || 'Username is required.'; })];
|
|
206
213
|
case 2:
|
|
207
|
-
user =
|
|
214
|
+
user = _j.sent();
|
|
208
215
|
return [3 /*break*/, 4];
|
|
209
216
|
case 3:
|
|
210
217
|
hint = passwordStdin
|
|
@@ -215,7 +222,7 @@ var authLogin = function (target_1) {
|
|
|
215
222
|
if (!passwordStdin) return [3 /*break*/, 6];
|
|
216
223
|
return [4 /*yield*/, readPasswordFromStdin()];
|
|
217
224
|
case 5:
|
|
218
|
-
pass =
|
|
225
|
+
pass = _j.sent();
|
|
219
226
|
if (!pass) {
|
|
220
227
|
throw new Error("--password-stdin was specified but no password was read from stdin. " +
|
|
221
228
|
"Pipe the password, e.g. echo \"$SAS_PASSWORD\" | sasjs auth login --password-stdin -t ".concat(target.name, "."));
|
|
@@ -237,23 +244,35 @@ var authLogin = function (target_1) {
|
|
|
237
244
|
}
|
|
238
245
|
})];
|
|
239
246
|
case 8:
|
|
240
|
-
promptedPass = (
|
|
247
|
+
promptedPass = (_j.sent()).pass;
|
|
241
248
|
pass = promptedPass;
|
|
242
249
|
return [3 /*break*/, 10];
|
|
243
250
|
case 9: throw new Error("A SAS password is required but was not provided. " +
|
|
244
251
|
"Set the SAS_PASSWORD environment variable, or pipe the password and use --password-stdin " +
|
|
245
252
|
"(e.g. echo \"$SAS_PASSWORD\" | sasjs auth login --password-stdin -t ".concat(target.name, ")."));
|
|
246
|
-
case 10:
|
|
253
|
+
case 10:
|
|
254
|
+
if (!(target.serverType === utils_1.ServerType.Sasjs)) return [3 /*break*/, 13];
|
|
255
|
+
return [4 /*yield*/, (0, utils_2.getTokensWithSasjsLogin)(target, user, pass, process.env.SASJS_CLIENT)];
|
|
247
256
|
case 11:
|
|
248
|
-
_a =
|
|
249
|
-
return [4 /*yield*/, (0, utils_2.
|
|
257
|
+
_a = _j.sent(), access_token_1 = _a.access_token, refresh_token_1 = _a.refresh_token, client = _a.client, displayName = _a.displayName;
|
|
258
|
+
return [4 /*yield*/, (0, utils_2.saveTokens)(target.name, access_token_1, refresh_token_1, client)];
|
|
250
259
|
case 12:
|
|
251
|
-
|
|
260
|
+
_j.sent();
|
|
261
|
+
(_e = process.logger) === null || _e === void 0 ? void 0 : _e.success("Logged in as ".concat(displayName || user, " on ").concat(target.serverUrl, "."));
|
|
262
|
+
(_f = process.logger) === null || _f === void 0 ? void 0 : _f.info("The access token is valid for the duration configured on the server " +
|
|
263
|
+
"(24 hours by default). When it expires, re-run 'sasjs auth login -t ".concat(target.name, "'."));
|
|
264
|
+
return [2 /*return*/];
|
|
265
|
+
case 13: return [4 /*yield*/, (0, utils_2.getTokensWithPasswordGrant)(target, user, pass)];
|
|
266
|
+
case 14:
|
|
267
|
+
_b = _j.sent(), access_token = _b.access_token, refresh_token = _b.refresh_token;
|
|
268
|
+
return [4 /*yield*/, (0, utils_2.fetchLoggedInUser)(target, access_token)];
|
|
269
|
+
case 15:
|
|
270
|
+
_c = _j.sent(), id = _c.id, name = _c.name;
|
|
252
271
|
return [4 /*yield*/, (0, utils_2.saveTokens)(target.name, access_token, refresh_token || '')];
|
|
253
|
-
case
|
|
254
|
-
|
|
255
|
-
(
|
|
256
|
-
(
|
|
272
|
+
case 16:
|
|
273
|
+
_j.sent();
|
|
274
|
+
(_g = process.logger) === null || _g === void 0 ? void 0 : _g.success("Logged in as ".concat(id || 'unknown user').concat(name ? " (".concat(name, ")") : '', " on ").concat(target.serverUrl, "."));
|
|
275
|
+
(_h = process.logger) === null || _h === void 0 ? void 0 : _h.info("The access token is valid for the duration configured on the server (12 hours by default). " +
|
|
257
276
|
"When it expires, re-run 'sasjs auth login -t ".concat(target.name, "'."));
|
|
258
277
|
return [2 /*return*/];
|
|
259
278
|
}
|
|
@@ -103,7 +103,7 @@ var utils_2 = require("@sasjs/utils");
|
|
|
103
103
|
// Mock the three orchestration dependencies that authLogin delegates to.
|
|
104
104
|
// We spy on the utils barrel (../../utils) which re-exports from auth.ts and
|
|
105
105
|
// config.ts, so the spies are the same references login.ts resolves at runtime.
|
|
106
|
-
jest.mock('../../../utils', function () { return (__assign(__assign({}, jest.requireActual('../../../utils')), { getTokensWithPasswordGrant: jest.fn(), fetchLoggedInUser: jest.fn(), saveTokens: jest.fn() })); });
|
|
106
|
+
jest.mock('../../../utils', function () { return (__assign(__assign({}, jest.requireActual('../../../utils')), { getTokensWithPasswordGrant: jest.fn(), getTokensWithSasjsLogin: jest.fn(), fetchLoggedInUser: jest.fn(), saveTokens: jest.fn() })); });
|
|
107
107
|
var viyaTarget = new utils_1.Target({
|
|
108
108
|
name: 'viya-test',
|
|
109
109
|
serverType: utils_1.ServerType.SasViya,
|
|
@@ -111,6 +111,12 @@ var viyaTarget = new utils_1.Target({
|
|
|
111
111
|
appLoc: '/Public/test',
|
|
112
112
|
contextName: 'test context'
|
|
113
113
|
});
|
|
114
|
+
var sasjsTarget = new utils_1.Target({
|
|
115
|
+
name: 'sasjs-test',
|
|
116
|
+
serverType: utils_1.ServerType.Sasjs,
|
|
117
|
+
serverUrl: 'https://sasjs.example.com',
|
|
118
|
+
appLoc: '/Public/app'
|
|
119
|
+
});
|
|
114
120
|
var sas9Target = new utils_1.Target({
|
|
115
121
|
name: 'sas9-test',
|
|
116
122
|
serverType: utils_1.ServerType.Sas9,
|
|
@@ -157,10 +163,10 @@ describe('authLogin', function () {
|
|
|
157
163
|
// ---------------------------------------------------------------------------
|
|
158
164
|
// Guard 1: Non-Viya rejection
|
|
159
165
|
// ---------------------------------------------------------------------------
|
|
160
|
-
it('should reject when target.serverType is
|
|
166
|
+
it('should reject when target.serverType is SAS9', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
161
167
|
return __generator(this, function (_a) {
|
|
162
168
|
switch (_a.label) {
|
|
163
|
-
case 0: return [4 /*yield*/, expect(authLoginModule.authLogin(sas9Target)).rejects.toThrow("'sasjs auth login' is
|
|
169
|
+
case 0: return [4 /*yield*/, expect(authLoginModule.authLogin(sas9Target)).rejects.toThrow("'sasjs auth login' is supported for SASVIYA and SASJS targets.")];
|
|
164
170
|
case 1:
|
|
165
171
|
_a.sent();
|
|
166
172
|
expect(utilsModule.getTokensWithPasswordGrant).not.toHaveBeenCalled();
|
|
@@ -672,4 +678,98 @@ describe('authLogin', function () {
|
|
|
672
678
|
}
|
|
673
679
|
});
|
|
674
680
|
}); });
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
// SASJS: local login against a SASjs Server target
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
describe('SASJS target', function () {
|
|
685
|
+
beforeEach(function () {
|
|
686
|
+
delete process.env.SASJS_CLIENT;
|
|
687
|
+
utilsModule.getTokensWithSasjsLogin.mockResolvedValue({
|
|
688
|
+
access_token: 'sasjs-access',
|
|
689
|
+
refresh_token: 'sasjs-refresh',
|
|
690
|
+
client: 'client-id-1',
|
|
691
|
+
displayName: 'SASJS Test User'
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
afterEach(function () {
|
|
695
|
+
delete process.env.SASJS_CLIENT;
|
|
696
|
+
});
|
|
697
|
+
it('should mint tokens with the SASJS login flow instead of the password grant', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
698
|
+
return __generator(this, function (_a) {
|
|
699
|
+
switch (_a.label) {
|
|
700
|
+
case 0: return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
701
|
+
case 1:
|
|
702
|
+
_a.sent();
|
|
703
|
+
expect(utilsModule.getTokensWithPasswordGrant).not.toHaveBeenCalled();
|
|
704
|
+
expect(utilsModule.getTokensWithSasjsLogin).toHaveBeenCalledWith(expect.objectContaining({ name: 'sasjs-test' }), 'test-user', 'test-pass', undefined);
|
|
705
|
+
return [2 /*return*/];
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
}); });
|
|
709
|
+
it('should persist the client alongside the tokens', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
710
|
+
return __generator(this, function (_a) {
|
|
711
|
+
switch (_a.label) {
|
|
712
|
+
case 0: return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
713
|
+
case 1:
|
|
714
|
+
_a.sent();
|
|
715
|
+
expect(utilsModule.saveTokens).toHaveBeenCalledWith('sasjs-test', 'sasjs-access', 'sasjs-refresh', 'client-id-1');
|
|
716
|
+
expect(utilsModule.fetchLoggedInUser).not.toHaveBeenCalled();
|
|
717
|
+
return [2 /*return*/];
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
}); });
|
|
721
|
+
it('should pass SASJS_CLIENT through so CI can pin the client', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
722
|
+
return __generator(this, function (_a) {
|
|
723
|
+
switch (_a.label) {
|
|
724
|
+
case 0:
|
|
725
|
+
process.env.SASJS_CLIENT = 'pinned-client';
|
|
726
|
+
return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
727
|
+
case 1:
|
|
728
|
+
_a.sent();
|
|
729
|
+
expect(utilsModule.getTokensWithSasjsLogin).toHaveBeenCalledWith(expect.objectContaining({ name: 'sasjs-test' }), 'test-user', 'test-pass', 'pinned-client');
|
|
730
|
+
return [2 /*return*/];
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}); });
|
|
734
|
+
it('should authenticate from env vars with no TTY, as CI would', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
735
|
+
return __generator(this, function (_a) {
|
|
736
|
+
switch (_a.label) {
|
|
737
|
+
case 0:
|
|
738
|
+
process.env.SAS_USERNAME = 'ci-user';
|
|
739
|
+
process.env.SAS_PASSWORD = 'ci-pass';
|
|
740
|
+
Object.defineProperty(process.stdin, 'isTTY', {
|
|
741
|
+
value: false,
|
|
742
|
+
configurable: true
|
|
743
|
+
});
|
|
744
|
+
return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
745
|
+
case 1:
|
|
746
|
+
_a.sent();
|
|
747
|
+
expect(utilsModule.getTokensWithSasjsLogin).toHaveBeenCalledWith(expect.objectContaining({ name: 'sasjs-test' }), 'ci-user', 'ci-pass', undefined);
|
|
748
|
+
expect(utils_2.getString).not.toHaveBeenCalled();
|
|
749
|
+
expect(prompts_1.default).not.toHaveBeenCalled();
|
|
750
|
+
return [2 /*return*/];
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}); });
|
|
754
|
+
it('should fall back to the username when the server reports no display name', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
755
|
+
var success;
|
|
756
|
+
return __generator(this, function (_a) {
|
|
757
|
+
switch (_a.label) {
|
|
758
|
+
case 0:
|
|
759
|
+
;
|
|
760
|
+
utilsModule.getTokensWithSasjsLogin.mockResolvedValue({
|
|
761
|
+
access_token: 'sasjs-access',
|
|
762
|
+
refresh_token: 'sasjs-refresh',
|
|
763
|
+
client: 'client-id-1'
|
|
764
|
+
});
|
|
765
|
+
success = jest.spyOn(process.logger, 'success');
|
|
766
|
+
return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
767
|
+
case 1:
|
|
768
|
+
_a.sent();
|
|
769
|
+
expect(success).toHaveBeenCalledWith(expect.stringContaining('Logged in as test-user on'));
|
|
770
|
+
return [2 /*return*/];
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}); });
|
|
774
|
+
});
|
|
675
775
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CompileTree, Target } from '@sasjs/utils';
|
|
2
2
|
import { TestFlow } from '../../../types';
|
|
3
|
+
/** The coverage report `sasjs compile` writes to the build folder. */
|
|
4
|
+
export declare const coverageFileName = "coverage.json";
|
|
3
5
|
/**
|
|
4
6
|
* Resolves the relative destination fragment for a compiled test file inside
|
|
5
7
|
* `sasjsbuild/tests` - either just the file's basename (saveToRoot, used for
|
|
@@ -64,7 +64,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
64
64
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
65
65
|
};
|
|
66
66
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
|
-
exports.compileTestFlow = exports.getTestFileDestinationFragment = void 0;
|
|
67
|
+
exports.compileTestFlow = exports.getTestFileDestinationFragment = exports.coverageFileName = void 0;
|
|
68
68
|
exports.compileTestFile = compileTestFile;
|
|
69
69
|
exports.copyTestMacroFiles = copyTestMacroFiles;
|
|
70
70
|
var utils_1 = require("@sasjs/utils");
|
|
@@ -75,6 +75,8 @@ var config_1 = require("../../../utils/config");
|
|
|
75
75
|
var file_1 = require("../../../utils/file");
|
|
76
76
|
var _1 = require("./");
|
|
77
77
|
var getFileName = function (filePath) { return path_1.default.parse(filePath).base; };
|
|
78
|
+
/** The coverage report `sasjs compile` writes to the build folder. */
|
|
79
|
+
exports.coverageFileName = 'coverage.json';
|
|
78
80
|
/**
|
|
79
81
|
* Resolves the relative destination fragment for a compiled test file inside
|
|
80
82
|
* `sasjsbuild/tests` - either just the file's basename (saveToRoot, used for
|
|
@@ -232,7 +234,7 @@ var compileTestFlow = function (target) { return __awaiter(void 0, void 0, void
|
|
|
232
234
|
}); };
|
|
233
235
|
exports.compileTestFlow = compileTestFlow;
|
|
234
236
|
var printTestCoverage = function (testFlow, buildDestinationFolder, target) { return __awaiter(void 0, void 0, void 0, function () {
|
|
235
|
-
var toCover, covered, extraTests, serviceFolder, collectCoverage, jobFolder, macroFolders, filter,
|
|
237
|
+
var toCover, covered, extraTests, serviceFolder, collectCoverage, jobFolder, macroFolders, filter, coveredServices, servicesToCover, coveredJobs, jobsToCover, coveredMacros, macrosToCover, coverage, markCoverage, summarize, formatCoverage, summary, report;
|
|
236
238
|
var _a, _b, _c;
|
|
237
239
|
return __generator(this, function (_d) {
|
|
238
240
|
switch (_d.label) {
|
|
@@ -315,74 +317,52 @@ var printTestCoverage = function (testFlow, buildDestinationFolder, target) { re
|
|
|
315
317
|
filter = function (files, type) {
|
|
316
318
|
return files.filter(function (file) { return new RegExp("^".concat(type)).test(file); });
|
|
317
319
|
};
|
|
318
|
-
coverage = {};
|
|
319
|
-
notCovered = toCover.filter(function (tc) { return !covered.includes(tc); });
|
|
320
320
|
coveredServices = filter(covered, types_1.CoverageType.service);
|
|
321
321
|
servicesToCover = filter(toCover, types_1.CoverageType.service);
|
|
322
|
-
notCoveredServices = filter(notCovered, types_1.CoverageType.service);
|
|
323
|
-
notCoveredServices.forEach(function (file, i) {
|
|
324
|
-
coverage[file] = {
|
|
325
|
-
Type: types_1.CoverageType.service,
|
|
326
|
-
Coverage: types_1.CoverageState.notCovered
|
|
327
|
-
};
|
|
328
|
-
});
|
|
329
322
|
coveredJobs = filter(covered, types_1.CoverageType.job);
|
|
330
323
|
jobsToCover = filter(toCover, types_1.CoverageType.job);
|
|
331
|
-
notCoveredJobs = filter(notCovered, types_1.CoverageType.job);
|
|
332
|
-
notCoveredJobs.forEach(function (file, i) {
|
|
333
|
-
coverage[file] = {
|
|
334
|
-
Type: types_1.CoverageType.job,
|
|
335
|
-
Coverage: types_1.CoverageState.notCovered
|
|
336
|
-
};
|
|
337
|
-
});
|
|
338
324
|
coveredMacros = filter(covered, types_1.CoverageType.macro);
|
|
339
325
|
macrosToCover = filter(toCover, types_1.CoverageType.macro);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
: macro;
|
|
355
|
-
});
|
|
356
|
-
return [2 /*return*/];
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
}); })];
|
|
360
|
-
case 5:
|
|
361
|
-
_d.sent();
|
|
362
|
-
_d.label = 6;
|
|
363
|
-
case 6:
|
|
364
|
-
notCoveredMacros.forEach(function (file, i) {
|
|
365
|
-
coverage[file] = {
|
|
366
|
-
Type: types_1.CoverageType.macro,
|
|
367
|
-
Coverage: types_1.CoverageState.notCovered
|
|
368
|
-
};
|
|
369
|
-
});
|
|
326
|
+
coverage = {};
|
|
327
|
+
markCoverage = function (type, filesToCover, coveredFiles) {
|
|
328
|
+
return filesToCover.forEach(function (file) {
|
|
329
|
+
coverage[file] = {
|
|
330
|
+
Type: type,
|
|
331
|
+
Coverage: coveredFiles.includes(file)
|
|
332
|
+
? types_1.CoverageState.covered
|
|
333
|
+
: types_1.CoverageState.notCovered
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
markCoverage(types_1.CoverageType.service, servicesToCover, coveredServices);
|
|
338
|
+
markCoverage(types_1.CoverageType.job, jobsToCover, coveredJobs);
|
|
339
|
+
markCoverage(types_1.CoverageType.macro, macrosToCover, coveredMacros);
|
|
370
340
|
extraTests.forEach(function (file) {
|
|
371
341
|
coverage[file] = {
|
|
372
342
|
Type: types_1.CoverageType.test,
|
|
373
343
|
Coverage: types_1.CoverageState.standalone
|
|
374
344
|
};
|
|
375
345
|
});
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
346
|
+
summarize = function (cov, toCov) { return ({
|
|
347
|
+
covered: cov.length,
|
|
348
|
+
total: toCov.length,
|
|
349
|
+
percentage: toCov.length ? Math.round((cov.length / toCov.length) * 100) : 0
|
|
350
|
+
}); };
|
|
351
|
+
formatCoverage = function (type, summary) {
|
|
380
352
|
var _a;
|
|
381
|
-
return
|
|
382
|
-
? (_a = process.logger) === null || _a === void 0 ? void 0 : _a.info("".concat(type, " coverage: ").concat(covered
|
|
353
|
+
return summary.total
|
|
354
|
+
? (_a = process.logger) === null || _a === void 0 ? void 0 : _a.info("".concat(type, " coverage: ").concat(summary.covered, "/").concat(summary.total, " (").concat(chalk_1.default.greenBright("".concat(summary.percentage, "%")), ")"))
|
|
383
355
|
: null;
|
|
384
356
|
};
|
|
357
|
+
summary = {
|
|
358
|
+
services: summarize(coveredServices, servicesToCover),
|
|
359
|
+
jobs: summarize(coveredJobs, jobsToCover),
|
|
360
|
+
macros: summarize(coveredMacros, macrosToCover),
|
|
361
|
+
overall: summarize(covered, toCover)
|
|
362
|
+
};
|
|
385
363
|
(_a = process.logger) === null || _a === void 0 ? void 0 : _a.info('Test coverage:');
|
|
364
|
+
// The table renders the same map coverage.json serialises, so a reader sees
|
|
365
|
+
// every file - covered rows included - under the key the report uses.
|
|
386
366
|
(_b = process.logger) === null || _b === void 0 ? void 0 : _b.table(Object.keys(coverage).map(function (key) { return [
|
|
387
367
|
key,
|
|
388
368
|
coverage[key].Type,
|
|
@@ -390,13 +370,21 @@ var printTestCoverage = function (testFlow, buildDestinationFolder, target) { re
|
|
|
390
370
|
]; }), {
|
|
391
371
|
head: ['File', 'Type', 'Coverage']
|
|
392
372
|
});
|
|
393
|
-
formatCoverage('Services',
|
|
394
|
-
formatCoverage('Jobs',
|
|
395
|
-
formatCoverage('Macros',
|
|
396
|
-
formatCoverage('Overall',
|
|
373
|
+
formatCoverage('Services', summary.services);
|
|
374
|
+
formatCoverage('Jobs', summary.jobs);
|
|
375
|
+
formatCoverage('Macros', summary.macros);
|
|
376
|
+
formatCoverage('Overall', summary.overall);
|
|
397
377
|
(_c = process.logger) === null || _c === void 0 ? void 0 : _c.log('');
|
|
398
|
-
return [4 /*yield*/, (0, utils_1.createFile)(path_1.default.join(buildDestinationFolder, 'testFlow.json'), JSON.stringify(testFlow, null, 2))
|
|
399
|
-
|
|
378
|
+
return [4 /*yield*/, (0, utils_1.createFile)(path_1.default.join(buildDestinationFolder, 'testFlow.json'), JSON.stringify(testFlow, null, 2))
|
|
379
|
+
// The numbers are already computed and rendered above, so the report costs
|
|
380
|
+
// one small file - which lets a CI job publish the figures instead of
|
|
381
|
+
// scraping the log.
|
|
382
|
+
];
|
|
383
|
+
case 5:
|
|
384
|
+
_d.sent();
|
|
385
|
+
report = { summary: summary, files: coverage };
|
|
386
|
+
return [4 /*yield*/, (0, utils_1.createFile)(path_1.default.join(buildDestinationFolder, exports.coverageFileName), JSON.stringify(report, null, 2))];
|
|
387
|
+
case 6:
|
|
400
388
|
_d.sent();
|
|
401
389
|
return [2 /*return*/];
|
|
402
390
|
}
|