Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Screenshots, logs and video recordings of tests #734

Merged
merged 113 commits into from
Jun 20, 2018
Merged

Conversation

noomorph
Copy link
Collaborator

@noomorph noomorph commented May 15, 2018

The pull request aims to add the following features:

  1. Automatic screen recording on iOS/Android with two configurable options: --artifacts-location [rootDir] (where videos will be saved) and --record-videos [all|failing|none] behavior (none is default and means that recording is disabled).
  2. Automatic screenshots (beforeEach and afterEach) on iOS/Android with two configurable options: --artifacts-location [rootDir] (where screenshots will be saved) and --take-screenshots [all|failing|none] behavior (none is default and means that screenshotting is disabled).
  3. Automatic log recording during test on iOS/Android with two configurable options: --artifacts-location [rootDir] (where logs will be saved) and --record-logs [all|failing|none] behavior (none is default and means that recording is disabled).
  4. Out-of-box support for [mocha] and [jest] test runners:
  • detox init -r mocha and detox init -r jest should scaffold configuration files which are sufficient for the new artifact recording features and (hopefully) flexible enough not to require manual updates on the user side.
  • the latter is being solved by introducing detox<->mocha and detox<->jest adapters which encapsulate the tricky parts to enable executing long-running hooks (e.g. before (beforeAll), beforeEach, afterEach, after (afterAll) in mocha and jest) together with ability to receive current running test status and name (not trivial thing in jest). That's a thing strongly required by the artifacts feature.
  1. Artifacts feature should be fairly decoupled from detox internals - decoupled enough to grant a possibility to extract it to a separate plugin with ease in more distant future (if we'll decide we need it).

Current status

The PR’s author (@noomorph) has returned from the vacation. He is working on documentation this week, stabilizing code, improving error messages and unit tests also.

TODO list

  • make unit tests pass again
  • complete and fix Android video recording
  • complete and fix taking screenshots on Android
  • complete and fix taking screenshots on iOS
  • complete and fix iOS video recording
  • pass initial pull request review
  • rewrite iOS logger to follow .start().stop().save().discard() interface
  • write adb logcat-based logger implementation with the aforementioned interface
  • rewrite to make adding new type of artifacts even easier
  • synchronous .kill() interface when users press Ctrl+C (SIGINT or other sort of emergency exit).
  • improve adb logcat logger - keep recording logs even after device relaunch (when pid changes)
  • simple exception handling in onStart, onBeforeTest, ... hooks
  • revert .kill() interface idea in favor of state machine in base ArtifactRecording.js class
  • fix unknown artifact plugin name in onIdleCallback error
  • fix upload_artifact.sh script (from previous PR) error (aws: command not found) on Travis CI
  • fix bug with first screenshot on iOS
  • complete unit tests for core functionality ([+coverage] unit tests for ArtifactsManager.js and ArtifactPlugin.js #794)

Documentation:

  • update Getting Started.
  • update Migration Guide.
  • update test lifecycle about beforeEach and afterEach
  • update detox object about detox.beforeEach(testSummary) and detox.afterEach(testSummary).
  • update Jest Guide about detox init -r jest.
  • update detox-cli with new artifact flags and breaking change that --record-logs all should be specified explicitly in order to retain the previous behavior.
  • update Enabling artifacts section with first steps and troubleshooting
  • add a pitfalls subsection: e.g. Ctrl+C does not work correctly with Jest, Metal is not supported on VM, or maybe Increased flakiness with artifacts recording, and so on...

Nice to haves:

  • extend configuration for screenshot plugin - add an ability to specify at which phase you want screenshots to be taken: beforeEach, afterEach or both. E.g., all:beforeEach,afterEach means that screenshots will be taken in every test two times: in a global beforeEach and in a global afterEach. One more example, failing:afterEach will mean that screenshots will be taken only on failing tests in a global afterEach. In future we might add beforeTest and afterTest phases which will surround test() or it() statement.
  • integration tests for artifact plugins
  • merge from branch e2e suite fordetox/src/runners/jest/DetoxJestAdapter.js
  • merge from branch e2e suite for detox/src/runners/mocha/DetoxMochaAdapter.js
  • improve logging in verbose mode (e.g. add prefix by process name and pid to exec logs)
  • add extensive logging for artifacts plugins
  • specific exception handling (e.g. Metal is not supported on VM)

Links:

Hopefully, I'll be moving quite-quite fast.
Stay tuned.

@noomorph noomorph requested a review from rotemmiz as a code owner May 15, 2018 17:34
@noomorph
Copy link
Collaborator Author

I thought it might be curious to share a video of detox/test project run (from yesterday) with android video recording turned on.

https://youtu.be/mLVw3jowr4k

click to see video

A nice improvement (since the former PR) is that Detox copies recorded videos in parallel. Namely, we don't wait for all tests to complete before we start pulling videos from the device.

P. S. On the other hand, I have a silly concern whether framerates are apt to drop (...or maybe not?) when adb screenrecord and adb pull run concurrently. If this proves to be an issue, I might revert the behavior to the former one or alternatively, add an extra flag, but, in any case, this "gut feeling" needs to be either proved or disproved.

Copy link
Member

@rotemmiz rotemmiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, and thanks for the new and improved candy, adapters, detox-init, custom errors and prints.

I do have some questions regarding implementation of the artifact manager and peripherals (see notes).

@@ -0,0 +1,44 @@
const _ = require('lodash');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the thing, not v1 vs v2, its a replacement, no going back. just call the kid in its name (and path): detox/src/artifacts/ArtifactsManager.js

this.hooks = [];
}

registerHooks(hooks) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting handling of malformed hooks

}
}

module.exports = ArtifactsManager;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice and clean, I like it <3

.registerHooks(resolve.artifacts.hooks.video(detoxApi));
}),
},
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused with index.js

  1. Why did you choose resolveing once (using lodash)? can't this be a simple object that will be instantiated once somewhere in the init process?
  2. Android and iOS logic are mixed in this class, why is that ?
  3. There's a factory (actual). What is the reason to go over the selection phase again if DeviceDriver is already initiated ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also looks like VideoRecorderHooks.js and ScreenshotterHooks.js share some logic, but mostly, if we want to add a new artifact (let's say performance counters from Detox Instruments), it seems as though we would need to clone one of those classes implementation. right ?

Wouldn't it make sense to put most of this hooks logic in ArtifactsManager, and only implement start(), stop(), copy(), discard() methods (did I forget anything) for each artifact ?

@@ -0,0 +1,15 @@
const ADBLogcatTailRecording = require('./ADBLogcatTailRecording');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the use of ADBLogcatLogger ? What's the benefits of using it on just using ADBLogcatTailRecording.js instance controlled by ArtifactManager ?

@@ -0,0 +1,14 @@
class NoopVideoRecording {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need those empty noop classes ? can we just not instantiate the real classes when users ask not to do generate artifacts ?

const _ = require('lodash');
const fsext = require('fs-extra');

class AndroidToolsLocator {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you don't use this class, what is the thinking behind it? what makes it better than the current implementation where every command line wrapper handles finding the bin path by itself ?

@@ -0,0 +1,30 @@
const DetoxRuntimeError = require('../errors/DetoxRuntimeError');

This comment was marked as resolved.

/*
Trim filename to match filesystem limits (usually, not longer than 255 chars)
*/
function constructSafeFilename(prefix = '', trimmable = '', suffix = '') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usage is a bit convoluted, it took me too much time to understand how this works.
Where does the suffix comes from? It's hard for me to follow.

const testArtifactsDirname = constructSafeFilename(testIndexPrefix, testSummary.fullName);
const artifactFilename = constructSafeFilename(artifactName);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no use cases with suffixes at the moment, to be honest. I just thought it would make more sense to write a signature like (prefix, trimmable, suffix) so it tells about the intent of function which is basically return prefix + trimmable + suffix or in a worse case return prefix + trimmable.slice(-maxLen) + suffix.

Maybe the source of your convolution is a multimethod implementation which treats a single parameter as a second parameter? I have no objections to simplify it, so it will look like:

const testArtifactsDirname = constructSafeFilename(testIndexPrefix, testSummary.fullName);
const artifactFilename = constructSafeFilename('', artifactName);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed multimethod like I said before. Now signature should look less sophisticated, hopefully.

@@ -0,0 +1,10 @@
const sleep = require('./sleep');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to test this. you can put it in ignore list

@noomorph
Copy link
Collaborator Author

@rotemmiz , I made a (relatively) minor change to artifacts API.

record('artifact path').start().stop().save() became record().start().stop().save('artifact path').

It gives us a superpower to give different names to artifacts depending on test status (passed|failed). Hope you'll like it: ✔ 1. Passing test and ✗ 2. Failing test. 😛

This becomes very handy with option --record-whatever all.

screen shot 2018-05-18 at 19 42 35

@noomorph
Copy link
Collaborator Author

🎉 I've got iOS simulator logging working, hooray!

@noomorph
Copy link
Collaborator Author

@rotemmiz, check out the verbosity level I did for Android logs:

test.log

@noomorph
Copy link
Collaborator Author

Quick update. I've removed artifacts folders' numbering to avoid potential issues with parallel e2e test execution on multiple devices.

Now the folders look this way:

screenshot from 2018-05-21 19-00-29

]);
}

_createLogRecorderLifecycle(logRecorder) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three functions look so similar, can't they be merged into one function ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not that easy. 2 are using RecorderLifecycle (different params: startup video is not recorded, opposed to the startup log) and 1 is using SnapshotterLifecycle. Merging all lifecycle into one heavy and parameter-rich is a doubtful endeavor, to my mind. Might complicate understanding of the implementation.

@@ -0,0 +1,137 @@
const _ = require('lodash');
const npmlog = require('npmlog');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we usually use const log = require('npmlog');


async doDiscard() {}

_assertRecordingIsNotBeingResumed() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not a user facing API, it's a one time implementation, do we really need all those assertions ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right. Now it is not user-facing, but honestly I was thinking that we wanted to make it available in next versions.

I think we indeed can remove most of the checks, but on other hand they served me a great debugging value whilst writing lifecycles. If anybody goes to change something there or adds a new lifecycle, it can help while testing.

Secondly, sometimes we might need a smarter logic inside. E.g., if on SIGINT we have to clean up allocated temporary artifact files, it is easier to call .discard() from ArtifactsManager for each artifact rather than implement status checks to see if we need to .stop() it before .discard()-ing it. Smarter implementation of .discard() gives less headache when you just need to .discard() something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though that lifecycle is controlled by ArtifactManager, its a one time implementation, am I wrong ?

Copy link
Collaborator Author

@noomorph noomorph May 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, but indirectly.
I prefer to keep lifecycles of a SnapshotArtifact and a RecordingArtifact as separate entities.
They are different enough in my opinion to not try to stitch them together.
In any case, ArtifactsManager is responsible for overall hooks (onStart, onExit, ...) execution and it has a couple of global (cross-lifecycle) concerns:

  • It keeps registering artifacts, so it can discard them in an emergency.
  • It provides enqueueFinalizationTask method to lifecycles - to coordinate finalization, e.g. to make it less CPU-intensive.

In other words, ArtifactsManager alone has enough of a headache to handle, so it delegates minutiae to "smaller" and more specific lifecycles per artifact type.

@@ -0,0 +1,7 @@
const RecordingArtifact = require('./RecordingArtifact');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the test, we may just as well ignore it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and add to ignored files in package.json

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a unit test.

@@ -0,0 +1,7 @@
const SnapshotArtifact = require('./SnapshotArtifact');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same goes here, we can ignore it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a unit test.

return;
}

const spec = Object.freeze({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This object is going inside Detox implementation, which just reads from it (correct me if I'm wrong please)
Why do we need to freeze it ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we get rid of all Object.freeze, especially if the consumers are us.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point.

return parseInt(stdout, 10);
}

screencap(deviceId, path) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this supposed to be an async function ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. Missed. Thanks.

return this.spawn(deviceId, ['logcat', ...logcatArgs]);
}

pull(deviceId, src, dst = '') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async ?

return this.adbCmd(deviceId, `pull "${src}" "${dst}"`);
}

rm(deviceId, path, force = false) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async ?

}

_createTail(file, prefix) {
const tail = new Tail(file, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid tailing those files might be CPU intensive, let's think together if there's something out of process that we can do to avoid reading line by line.
Previously we have redirected std to files, and collected the files after each test. It's ugly, I agree, but much more efficient.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can delegate it to a real *nix tail > test.log, since it should be more lightweight than Node.js.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure if this is too intensive. Need to measure. And I'll check it on underpowered macbook 12 for issues.

@rotemmiz rotemmiz merged commit e0f4614 into master Jun 20, 2018
@rotemmiz rotemmiz deleted the feature/test-artifacts branch June 20, 2018 07:39
@rotemmiz
Copy link
Member

🎉

@luisnaranjo733
Copy link
Contributor

This is awesome. I think this will bring a ton of value to detox and enable QA scenarios that were previously not possible.

I'm very interested in using detox specifically for this functionality.
Judging by when this was merged and what I'm seeing in npmjs.com, I assume this will be available for the first time in 8.0.0-alpha.2.

Is there an ETA on that? Also, do you know when you are expecting to ship a stable 8.0.0 release?

@rotemmiz rotemmiz changed the title [WIP] [new] Screenshots and screen recordings of tests Screenshots, logs and video recordings of tests Jun 27, 2018
@juddey
Copy link

juddey commented Jun 27, 2018

This is fantastic. Thank you. ❤️

@mCodex
Copy link

mCodex commented Jul 19, 2018

Awesome work, thank you!

@wix wix locked and limited conversation to collaborators Jul 23, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants