-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Conversation
I thought it might be curious to share a video of 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 |
There was a problem hiding this 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'); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
detox/src/artifacts/v2/index.js
Outdated
.registerHooks(resolve.artifacts.hooks.video(detoxApi)); | ||
}), | ||
}, | ||
}; |
There was a problem hiding this comment.
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
- Why did you choose
resolve
ing once (using lodash)? can't this be a simple object that will be instantiated once somewhere in the init process? - Android and iOS logic are mixed in this class, why is that ?
- There's a factory (
actual
). What is the reason to go over the selection phase again if DeviceDriver is already initiated ?
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
This comment was marked as resolved.
Sorry, something went wrong.
/* | ||
Trim filename to match filesystem limits (usually, not longer than 255 chars) | ||
*/ | ||
function constructSafeFilename(prefix = '', trimmable = '', suffix = '') { |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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.
detox/src/utils/sleep.test.js
Outdated
@@ -0,0 +1,10 @@ | |||
const sleep = require('./sleep'); |
There was a problem hiding this comment.
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
0cac087
to
20e419d
Compare
@rotemmiz , I made a (relatively) minor change to artifacts API.
It gives us a superpower to give different names to artifacts depending on test status ( This becomes very handy with option --record-whatever all. |
🎉 I've got iOS simulator logging working, hooray! |
… from .record(path).stop().save() to .record().stop().save(path)
…on the rest of the project
7c6ecd5
to
a7cf806
Compare
]); | ||
} | ||
|
||
_createLogRecorderLifecycle(logRecorder) { |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid point.
detox/src/devices/android/ADB.js
Outdated
return parseInt(stdout, 10); | ||
} | ||
|
||
screencap(deviceId, path) { |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. Missed. Thanks.
detox/src/devices/android/ADB.js
Outdated
return this.spawn(deviceId, ['logcat', ...logcatArgs]); | ||
} | ||
|
||
pull(deviceId, src, dst = '') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async ?
detox/src/devices/android/ADB.js
Outdated
return this.adbCmd(deviceId, `pull "${src}" "${dst}"`); | ||
} | ||
|
||
rm(deviceId, path, force = false) { |
There was a problem hiding this comment.
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, { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
8b201c6
to
38f03d7
Compare
🎉 |
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. Is there an ETA on that? Also, do you know when you are expecting to ship a stable 8.0.0 release? |
This is fantastic. Thank you. ❤️ |
Awesome work, thank you! |
The pull request aims to add the following features:
--artifacts-location [rootDir]
(where videos will be saved) and--record-videos [all|failing|none]
behavior (none
is default and means that recording is disabled).--artifacts-location [rootDir]
(where screenshots will be saved) and--take-screenshots [all|failing|none]
behavior (none
is default and means that screenshotting is disabled).--artifacts-location [rootDir]
(where logs will be saved) and--record-logs [all|failing|none]
behavior (none
is default and means that recording is disabled).detox init -r mocha
anddetox 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.detox<->mocha
anddetox<->jest
adapters which encapsulate the tricky parts to enable executing long-running hooks (e.g.before (beforeAll)
,beforeEach
,afterEach
,after (afterAll)
inmocha
andjest
) together with ability to receive current running test status and name (not trivial thing injest
). That's a thing strongly required by the artifacts feature.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
.start().stop().save().discard()
interfaceadb logcat
-based logger implementation with the aforementioned interfaceSIGINT
or other sort of emergency exit).pid
changes)onStart
,onBeforeTest
, ... hooksArtifactRecording.js
classunknown
artifact plugin name inonIdleCallback
errorupload_artifact.sh
script (from previous PR) error (aws: command not found
) on Travis CIDocumentation:
beforeEach
andafterEach
detox.beforeEach(testSummary)
anddetox.afterEach(testSummary)
.detox init -r jest
.--record-logs all
should be specified explicitly in order to retain the previous behavior.Ctrl+C does not work correctly with Jest
,Metal is not supported on VM
, or maybeIncreased flakiness with artifacts recording
, and so on...Nice to haves:
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 addbeforeTest
andafterTest
phases which will surroundtest()
orit()
statement.detox/src/runners/jest/DetoxJestAdapter.js
detox/src/runners/mocha/DetoxMochaAdapter.js
exec
logs)Links:
Hopefully, I'll be moving quite-quite fast.
Stay tuned.