@testomatio/reporter 1.0.0-beta.4 → 1.1.0-beta

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.
@@ -15,6 +15,10 @@ class ArtifactStorage {
15
15
  this.storage = params?.toFile;
16
16
 
17
17
  if (this.storage) {
18
+ // this is fine to use when you start saving artifacts;
19
+ // but when you need to retrieve them, you will not know if there is "isFile" param,
20
+ // because you need to create a new instance of ArtifactStorage inside client
21
+ // so the solution is make it opposite to isMemory (which will be retrieved from global)
18
22
  this.isFile = true;
19
23
  this.isMemory = false;
20
24
  this.tmpDirFullpath = this.createTestomatTmpDir(this._tmpPrefix);
@@ -23,8 +27,10 @@ class ArtifactStorage {
23
27
  }
24
28
 
25
29
  static async storeToFile(tmpDirName, artifact, testSuffix = "test") {
30
+ // this assumes to use multiple files for each test. do we really want this?
26
31
  const suffix = TESTOMAT_ARTIFACT_SUFFIX + uuid.v4() + testSuffix;
27
32
  const dirpath = join(os.tmpdir(), tmpDirName);
33
+ // why json?
28
34
  const filepath = resolve(dirpath, `${suffix}.json`);
29
35
 
30
36
  return fs.promises.appendFile(filepath, JSON.stringify(artifact));
@@ -34,6 +40,7 @@ class ArtifactStorage {
34
40
  // TODO: mode for different pre-hooks. As variant - "all-tests" || "only_fail"
35
41
  // const mode = process.env.TESTOMAT_ARTIFACTS || "only_fail";
36
42
 
43
+ // you save the artifact without specifying its source - test id
37
44
  if (Array.isArray(global.testomatioArtifacts)) {
38
45
  debug("Saving artifacts to global storage");
39
46
 
@@ -71,6 +78,7 @@ class ArtifactStorage {
71
78
  return list;
72
79
  }
73
80
 
81
+ // returns all the content from all files; do we really need it?
74
82
  async tmpContents() {
75
83
  const contents = [];
76
84
 
@@ -103,6 +111,7 @@ class ArtifactStorage {
103
111
  }
104
112
  }
105
113
 
114
+ // no need to use multiple dirs; we can implement it later if required
106
115
  static tmpTestomatDirNames() {
107
116
  const subname = this._tmpPrefix || "tsmt_reporter";
108
117
 
@@ -113,6 +122,7 @@ class ArtifactStorage {
113
122
  }
114
123
 
115
124
  cleanup() {
125
+ // when you do a cleanup, I know nothing about isFile param
116
126
  if (this.isFile && this.tmpDirFullpath) {
117
127
  const tmpDirNames = ArtifactStorage.tmpTestomatDirNames();
118
128
 
@@ -33,6 +33,7 @@ if (MAJOR_VERSION < 3) {
33
33
  function CodeceptReporter(config) {
34
34
  let failedTests = [];
35
35
  let videos = [];
36
+ let traces = [];
36
37
  const reportTestPromises = [];
37
38
 
38
39
  const testTimeMap = {};
@@ -54,6 +55,7 @@ function CodeceptReporter(config) {
54
55
  event.dispatcher.on(event.all.before, () => {
55
56
  recorder.add('Creating new run', () => client.createRun());
56
57
  videos = [];
58
+ traces = [];
57
59
  });
58
60
 
59
61
  event.dispatcher.on(event.test.before, () => {
@@ -70,25 +72,13 @@ function CodeceptReporter(config) {
70
72
  });
71
73
 
72
74
  event.dispatcher.on(event.all.result, async () => {
75
+ debug('waiting for all tests to be reported');
73
76
  // all tests were reported and we can upload videos
74
77
  await Promise.all(reportTestPromises);
75
78
 
76
- if (videos.length && upload.isArtifactsEnabled()) {
77
- console.log(APP_PREFIX, `🎞️ Uploading ${videos.length} videos...`);
78
-
79
- const promises = [];
80
- for (const video of videos) {
81
- const { testId, title, path, type } = video;
82
- const file = { path, type, title };
83
- promises.push(
84
- client.addTestRun(undefined, {
85
- ...stripExampleFromTitle(title),
86
- test_id: testId,
87
- files: [file],
88
- }),
89
- );
90
- }
91
- await Promise.all(promises);
79
+ if (upload.isArtifactsEnabled()) {
80
+ uploadAttachments(client, videos, '🎞️ Uploading', 'video');
81
+ uploadAttachments(client, traces, '📁 Uploading', 'trace');
92
82
  }
93
83
 
94
84
  const status = failedTests.length === 0 ? STATUS.PASSED : STATUS.FAILED;
@@ -166,9 +156,11 @@ function CodeceptReporter(config) {
166
156
  }).then(pipes => {
167
157
  testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
168
158
 
159
+ debug("artifacts", artifacts);
160
+
169
161
  for (const aid in artifacts) {
170
- if (aid.startsWith('video')) videos.push({ testId, title, path: artifacts[aid], type: 'video/webm' });
171
- if (aid.startsWith('trace')) files.push({ testId, title, path: artifacts[aid], type: 'application/zip' });
162
+ if (aid.startsWith('video')) videos.push({ testId, title, path: artifacts[aid], type: 'video/webm' });
163
+ if (aid.startsWith('trace')) traces.push({ testId, title, path: artifacts[aid], type: 'application/zip' });
172
164
  }
173
165
  });
174
166
 
@@ -242,6 +234,24 @@ function CodeceptReporter(config) {
242
234
  });
243
235
  }
244
236
 
237
+ async function uploadAttachments(client, attachments, messagePrefix, attachmentType) {
238
+ if (attachments.length > 0) {
239
+ console.log(APP_PREFIX, `Attachments: ${messagePrefix} ${attachments.length} ${attachmentType}/-s ...`);
240
+
241
+ const promises = attachments.map(async (attachment) => {
242
+ const { testId, title, path, type } = attachment;
243
+ const file = { path, type, title };
244
+ return client.addTestRun(undefined, {
245
+ ...stripExampleFromTitle(title),
246
+ test_id: testId,
247
+ files: [file],
248
+ });
249
+ });
250
+
251
+ await Promise.all(promises);
252
+ }
253
+ }
254
+
245
255
  function parseTest(tags) {
246
256
  if (tags) {
247
257
  for (const tag of tags) {
@@ -1,6 +1,6 @@
1
1
  const TestomatClient = require('../client');
2
- const { STATUS } = require('../constants');
3
- const { parseTest, ansiRegExp } = require('../util');
2
+ const { STATUS, TESTOMAT_TMP_STORAGE } = require('../constants');
3
+ const { parseTest, ansiRegExp, fileSystem } = require('../util');
4
4
 
5
5
  class JestReporter {
6
6
  constructor(globalConfig, options) {
@@ -11,6 +11,16 @@ class JestReporter {
11
11
  this.client.createRun();
12
12
  }
13
13
 
14
+ static getIdOfCurrentlyRunningTest() {
15
+ // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
16
+ return parseTest(expect?.getState()?.currentTestName) || null; // eslint-disable-line
17
+ }
18
+
19
+ onRunStart() {
20
+ // clear tmp dir
21
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
22
+ }
23
+
14
24
  onTestResult(test, testResult) {
15
25
  if (!this.client) return;
16
26
 
@@ -47,6 +57,9 @@ class JestReporter {
47
57
  const { numFailedTests } = results;
48
58
  const status = numFailedTests === 0 ? STATUS.PASSED : STATUS.FAILED;
49
59
  this.client.updateRunStatus(status);
60
+
61
+ // clear tmp dir
62
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE.mainDir);
50
63
  }
51
64
  }
52
65
 
@@ -5,7 +5,7 @@ const chalk = require('chalk');
5
5
  const TestomatClient = require('../client');
6
6
  const { STATUS } = require('../constants');
7
7
  const { parseTest, specificTestInfo } = require('../util');
8
- const ArtifactStorage = require('../ArtifactStorage');
8
+ const ArtifactStorage = require('../_ArtifactStorageOld');
9
9
 
10
10
  const { EVENT_RUN_BEGIN, EVENT_RUN_END, EVENT_TEST_FAIL, EVENT_TEST_PASS, EVENT_TEST_PENDING } = Mocha.Runner.constants;
11
11
 
@@ -0,0 +1,25 @@
1
+ // const debug = require('debug')('@testomatio/reporter:logger');
2
+ const { DataStorage } = require('./dataStorage');
3
+
4
+ class ArtifactStorage {
5
+ constructor(params = {}) {
6
+ this.dataStorage = new DataStorage('artifact', params);
7
+
8
+ // singleton
9
+ if (!ArtifactStorage.instance) {
10
+ ArtifactStorage.instance = this;
11
+ }
12
+ }
13
+
14
+ save(data, context = null) {
15
+ this.dataStorage.putData(data, context);
16
+ }
17
+
18
+ get(context) {
19
+ this.dataStorage.getData(context);
20
+ }
21
+ }
22
+
23
+ ArtifactStorage.instance = null;
24
+
25
+ module.exports = new ArtifactStorage();
package/lib/client.js CHANGED
@@ -7,10 +7,12 @@ const { randomUUID } = require('crypto');
7
7
  const upload = require('./fileUploader');
8
8
  const { APP_PREFIX } = require('./constants');
9
9
  const pipesFactory = require('./pipe');
10
+ const logger = require('./logger');
10
11
 
11
12
  /**
12
13
  * @typedef {import('../types').TestData} TestData
13
14
  * @typedef {import('../types').RunStatus} RunStatus
15
+ * @typedef {import('../types').PipeResult} PipeResult
14
16
  */
15
17
 
16
18
  class Client {
@@ -33,7 +35,7 @@ class Client {
33
35
  /**
34
36
  * Used to create a new Test run
35
37
  *
36
- * @returns {Promise<void>} - resolves to Run id which should be used to update / add test
38
+ * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
37
39
  */
38
40
  createRun() {
39
41
  // all pipes disabled, skipping
@@ -46,6 +48,7 @@ class Client {
46
48
  };
47
49
 
48
50
  global.testomatioArtifacts = [];
51
+ // TODO: create global storage
49
52
 
50
53
  this.queue = this.queue
51
54
  .then(() => Promise.all(this.pipes.map(p => p.createRun(runParams))))
@@ -57,20 +60,21 @@ class Client {
57
60
 
58
61
  /**
59
62
  * Updates test status and its data
60
- *
63
+ *
61
64
  * @param {string|undefined} status
62
65
  * @param {TestData} [testData]
63
66
  * @param {string[]} [storeArtifacts]
64
- * @returns {Promise<void>}
67
+ * @returns {Promise<PipeResult[]>}
65
68
  */
66
69
  async addTestRun(status, testData, storeArtifacts = []) {
67
70
  // all pipes disabled, skipping
68
71
  if (!this.pipes.filter(p => p.isEnabled).length) return;
69
72
 
70
- if (!testData) testData = {
71
- title: 'Unknown test',
72
- suite_title: 'Unknown suite',
73
- }
73
+ if (!testData)
74
+ testData = {
75
+ title: 'Unknown test',
76
+ suite_title: 'Unknown suite',
77
+ };
74
78
 
75
79
  const {
76
80
  error = null,
@@ -99,13 +103,17 @@ class Client {
99
103
  stack = this.formatSteps(stack, steps);
100
104
  }
101
105
 
106
+ const testLogs = logger.getLogs(test_id);
107
+ debug(`Test logs for ${test_id}:\n`, testLogs);
108
+ stack += testLogs;
109
+
102
110
  if (Array.isArray(storeArtifacts) && storeArtifacts.length) {
103
- debug("CLIENT storeArtifact", storeArtifacts);
111
+ debug('CLIENT storeArtifact', storeArtifacts);
104
112
  files.push(...storeArtifacts);
105
113
  }
106
114
 
107
115
  if (Array.isArray(global.testomatioArtifacts)) {
108
- debug("CLIENT global[testomatioArtifacts]", global.testomatioArtifacts);
116
+ debug('CLIENT global[testomatioArtifacts]', global.testomatioArtifacts);
109
117
  files.push(...global.testomatioArtifacts);
110
118
  global.testomatioArtifacts = [];
111
119
  }
@@ -147,7 +155,7 @@ class Client {
147
155
  const result = await p.addTest(data);
148
156
  return { pipe: p.toString(), result };
149
157
  } catch (err) {
150
- console.log(APP_PREFIX, pipe.toString(), err);
158
+ console.log(APP_PREFIX, p.toString(), err);
151
159
  }
152
160
  }
153
161
  )));
@@ -160,7 +168,7 @@ class Client {
160
168
  * Updates the status of the current test run and finishes the run.
161
169
  * @param {RunStatus} status - The status of the current test run. Must be one of "passed", "failed", or "finished"
162
170
  * @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
163
- * @returns {Promise<void>} - A Promise that resolves when finishes the run.
171
+ * @returns {Promise<any>} - A Promise that resolves when finishes the run.
164
172
  */
165
173
  updateRunStatus(status, isParallel = false) {
166
174
  // all pipes disabled, skipping
@@ -171,8 +179,8 @@ class Client {
171
179
  this.queue = this.queue
172
180
  .then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
173
181
  .then(() => {
174
- debug("TOTAL uploaded files", this.totalUploaded);
175
-
182
+ debug('TOTAL uploaded files', this.totalUploaded);
183
+
176
184
  if (upload.isArtifactsEnabled() && this.totalUploaded > 0) {
177
185
  console.log(
178
186
  APP_PREFIX,
package/lib/constants.js CHANGED
@@ -3,6 +3,10 @@ const chalk = require('chalk');
3
3
  const APP_PREFIX = chalk.gray('[TESTOMATIO]');
4
4
  const TESTOMAT_ARTIFACT_SUFFIX = "testomatio_artifact_";
5
5
 
6
+ const TESTOMAT_TMP_STORAGE = {
7
+ mainDir: "testomatio_tmp",
8
+ }
9
+
6
10
  const CSV_HEADERS = [
7
11
  { id: 'suite_title', title: 'Suite_title' },
8
12
  { id: 'title', title: 'Title' },
@@ -21,6 +25,7 @@ const STATUS = {
21
25
  module.exports = {
22
26
  APP_PREFIX,
23
27
  TESTOMAT_ARTIFACT_SUFFIX,
28
+ TESTOMAT_TMP_STORAGE,
24
29
  CSV_HEADERS,
25
30
  STATUS,
26
31
  }
@@ -0,0 +1,172 @@
1
+ const debug = require('debug')('@testomatio/reporter:storage');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { join } = require('path');
6
+ const JestReporter = require('./adapter/jest');
7
+ const { TESTOMAT_TMP_STORAGE } = require('./constants');
8
+ const { fileSystem } = require('./util');
9
+
10
+ class DataStorage {
11
+ /**
12
+ * Creates data storage instance for specific data type.
13
+ * Stores data to global variable or to file depending on what is applicable for current test runner.
14
+ * dataType: 'log' | 'artifact' | ...
15
+ * @param {*} dataType storage type (like 'log', 'artifact'); could be any string, used only to define file path
16
+ * @param {*} params
17
+ */
18
+ constructor(dataType, params = {}) {
19
+ if (!dataType) throw new Error('Data type is required when creating data storage');
20
+ this.dataType = dataType || 'data';
21
+ this.dataDirName = `${dataType}s`;
22
+ this.isFileStorage = params?.isFileStorage ?? !global.testomatioDataStore;
23
+
24
+ /*
25
+ FYI:
26
+ If this storage instance is used within test runner, it works fine.
27
+ But if, for example, we create storage instance inside the testomatio client (to get stored data),
28
+ the environment could be different (not already a test runner env).
29
+ Thus, checking environment is only reasonable when you put data to starage
30
+ and potentially useless when get data from storage
31
+ */
32
+ const runningEnvironment = this.getRunningEnviroment();
33
+ if (runningEnvironment === 'jest') this.isFileStorage = true;
34
+
35
+ if (this.isFileStorage) {
36
+ this.dataDirPath = path.join(TESTOMAT_TMP_STORAGE.mainDir, this.dataDirName);
37
+ fileSystem.createDir(this.dataDirPath);
38
+ }
39
+
40
+ debug(`Data storage mode: ${this.isFileStorage ? 'file' : 'memory'}`);
41
+ }
42
+
43
+ // TODO: implement
44
+ getTestIdFromContext(context) { // eslint-disable-line
45
+ // eslint-disable-line
46
+ // return testId;
47
+ }
48
+
49
+ /**
50
+ * Try to define the running environment. Not 100% reliable and used as additional check
51
+ * @returns jest | mocha | ...
52
+ */
53
+ getRunningEnviroment() {
54
+ if (process.env.JEST_WORKER_ID) return 'jest';
55
+ // @ts-expect-error mocha could be undefined, its ok
56
+ if (typeof mocha !== 'undefined') return 'mocha';
57
+ return undefined;
58
+ }
59
+
60
+ /**
61
+ * Puts any data to storage (file or global variable)
62
+ * @param {*} data anything you want to store
63
+ * @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
64
+ * @returns
65
+ */
66
+ putData(data, context = null) {
67
+ let testId = null;
68
+ if (typeof context === 'string') {
69
+ testId = context;
70
+ } else {
71
+ // TODO: derive testId from context
72
+ // testId = context...
73
+ }
74
+
75
+ // try to get testId for Jest
76
+ testId = testId ?? JestReporter.getIdOfCurrentlyRunningTest();
77
+
78
+ // if testId is not provided, data is be saved to `{dataType}_other` file;
79
+ if (!testId) testId = 'other';
80
+
81
+ if (this.isFileStorage) {
82
+ this._putDataToFile(data, testId);
83
+ } else {
84
+ this._putDataToGlobalVar(data, testId);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Returns data, stored for specific testId (or data which was stored without test id specified)
90
+ *
91
+ * (Don't try to guess the execution environment (e.g. test runner) inside this method, it could be any)
92
+ * @param {*} context could be testId or any context (test, suite, etc) which is used to define testId
93
+ * @returns
94
+ */
95
+ getData(context) {
96
+ let testId = null;
97
+ if (typeof context === 'string') {
98
+ testId = context;
99
+ } else {
100
+ // TODO: derive testId from context
101
+ // testId = context...
102
+ }
103
+
104
+ if (!testId) testId = 'other';
105
+
106
+ if (this.isFileStorage) {
107
+ return this._getDataFromFile(testId);
108
+ }
109
+ if (global?.testomatioDataStore) {
110
+ return this._getDataFromGlobalVar(testId);
111
+ }
112
+ debug(`No ${this.dataType} data for test id ${testId} in both file and global variable`);
113
+ return null;
114
+ }
115
+
116
+ _getDataFromGlobalVar(testId) {
117
+ try {
118
+ if (global?.testomatioDataStore[this.dataDirName]) {
119
+ const testData = global.testomatioDataStore[this.dataDirName][testId];
120
+ debug(`Data for test id ${testId}:\n${testData}`);
121
+ return testData;
122
+ }
123
+ debug(`No ${this.dataType} data for test id ${testId}`);
124
+ return null;
125
+ } catch (e) {
126
+ // there could be no data, ignore
127
+ }
128
+ }
129
+
130
+ _getDataFromFile(testId) {
131
+ try {
132
+ const filepath = join(TESTOMAT_TMP_STORAGE.mainDir, `${this.dataType}_${testId}`);
133
+ if (fs.existsSync(filepath)) {
134
+ const testData = fs.readFileSync(filepath, 'utf-8');
135
+ debug(`Data for test id ${testId}:\n${testData}`);
136
+ return testData;
137
+ }
138
+ debug(`No ${this.dataType} data for test id ${testId}`);
139
+ return null;
140
+ } catch (e) {
141
+ // there could be no data, ignore
142
+ }
143
+ return null;
144
+ }
145
+
146
+ _putDataToGlobalVar(data, testId) {
147
+ debug('Saving data to global variable for test', testId, ':\n', data, '\n');
148
+ global.testomatioDataStore[this.dataDirName] = {};
149
+ global.testomatioDataStore[this.dataDirName][testId] = data;
150
+ }
151
+
152
+ _putDataToFile(data, testId) {
153
+ if (typeof data !== 'string') data = JSON.stringify(data);
154
+ const filename = `${this.dataType}_${testId}`;
155
+ const filepath = join(TESTOMAT_TMP_STORAGE.mainDir, filename);
156
+ debug('Saving data to file for test', testId, 'to', filepath, ':\n', data, '\n');
157
+
158
+ // TODO: handle multiple invocations of JSON.stringify.
159
+ // UPD: not actual because decided not to use it - it created extra wrapping quotes "" and // (escaped slashes)
160
+ fs.appendFileSync(filepath, data + os.EOL, 'utf-8');
161
+ }
162
+ }
163
+
164
+ module.exports.DataStorage = DataStorage;
165
+
166
+ // TODO: consider using fs promises instead of writeSync/appendFileSync to
167
+ // prevent blocking and improve performance (probably queue usage will be required)
168
+
169
+ // TODO: rewrite client - everything regarding storing artifacts
170
+ // TODO: try to define adapter inside client
171
+ // TODO: use .env
172
+ // TODO: ability to intercept multiple loggers