-
Notifications
You must be signed in to change notification settings - Fork 303
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
Add End-to-End Testing Utility #778
Conversation
This reverts commit ab06fe7.
test/run/checks.js
Outdated
@@ -0,0 +1,28 @@ | |||
|
|||
function usernameExists() { | |||
return ( process.env.E2EUSERNAME && process.env.E2EUSERNAME !== '' ) ? true : 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.
fun fact: this is equivalent to Boolean( process.env.E2EUSERNAME && … )
and also to !! (process.env.E2EU…)
just noting since the ternary is there a bit surprisingly - was there something odd about the condition that prevented us from casting it to a boolean?
anyway, not a big deal
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.
... was there something odd about the condition that prevented us from casting it to a boolean?
Nope - just my lack of experience writing idiomatic JavaScript. 😆Thanks for the suggestion - however casting to Boolean
is now unnecessary as I went ahead and used the suggested credentials check refactor in your comment below, which works pretty well.
try { | ||
checkCredentials(); | ||
|
||
const timestamp = ( new Date() ).toJSON().replace( /:/g, '-' ); |
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.
can we change this to be some explicit format? I find it near impossible to know the intention behind it, especially if for some reason the JSON serialization changes and we end up with formats that we don't expect.
const formatYMD = instant => {
const year = instant.getFullYear();
const month = `${ instant.getMonth() + 1 }`.padStart(2, '0');
const day = `${ instant.getDate() }`.padStart(2, '0');
return `${ year }-${ month }-${ date }`;
}
…
const timestamp = formatYMD( new Date() );
inline isn't so bad: it's still explicit but it is uglier on account of needing to cast numbers to strings
const now = new Date();
const timestamp = `${now.getFullYear()}-${`${now.getMonth() + 1}`.padStart(2,'0')}-${`${now.getDate()}`.padStart(2, '0')}`
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.
Agree - this snippet of code isn't intuitive at first glance. However in this specific case, all the snippet is doing is some very minor re-formatting and a comment to clarify is sufficient (add here):
// Replace `:` with `-` to format timestamp as YYYY-MM-DDTHH-MM-SS.mmmZ
I think a comment here is perfectly fine and creating an entire method to replicate all desired format components would add unnecessary complexity (and verbosity) to the file. If we choose to have a more customized date format in the future, we can create a separate method at that point.
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 comment is helpful enough since it shows what you wanted. my point was made though since I didn't expect .mmmZ
😄
test/run/init_app.js
Outdated
const { BUILT_APP_DIR } = require( './paths' ); | ||
|
||
function initApp( log ) { | ||
const startCmd = './' |
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.
was this more significant before? what's the purpose of splitting it from ./WordPress.com
?
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 was in anticipation of Windows-specific syntax, however I've been able to determine that will be unnecessary and we can stick with ./WordPress.com
, which is nice.
test/run/init_app.js
Outdated
} ); | ||
|
||
return app; | ||
} |
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.
Just playing around here…
function run( cwd, command, args, output ) {
const app = spawn( command, args, { stdio: [ 'ignore', output, output ], detached: true, cwd } );
app.on( 'error' => { throw new Error( `failed to initialize ${command}` ); } );
return app;
}
const app = run( BUILT_APP_DIR, './WordPress.com', [
'--disable-renderer-backgrounding',
'--disable-http-cache',
'--start-maximized',
'--remote-debugging-port=9222',
], appLog );
await delay(5000);
const driver = run( PROJECT_DIR, 'npx', [
'chromedriver',
'--port=9515',
'--verbose',
], driverLog );
There's a part of me feeling like we could make a simple abstraction over running the process and then eliminate these two init…
files. I'm nervous about having the straightforward logic of starting the tests be obscured by being hidden in a number of split up files.
Even the appLog
and driverLog
, for example, could be inlined in run-e2e-tests.js
For transparency, my anxiety about this is from the fact that the test runner systems are always one-off projects and have their own language and style. In these cases I definitely value having more stuff in one file where I can see it in my editor all at once over separating the pieces into separate files and modules.
While it may seem to disagree with my play above to introduce the process abstraction I'm still drawing a line between "this is a thing that spawns a process" and "start the app somehow" and "start the driver somehow." Whereas having the startup commands and CLI args in the test runner itself the mechanisms going on are obvious even if there's something hidden in the process abstraction (redirection of stdout
and stderr
). The pattern should be obvious enough from the cmd, [ arg1, arg2 ]
syntax
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's a part of me feeling like we could make a simple abstraction over running the process and then eliminate these two init… files
Agreed - having the various components initialized in separate files creates unnecessary indirection and overhead when navigating the script. I refactored to inline everything, and the proposed refactor for the driver
and app
initialization is a great idea. 👍 See refactor here.
test/run/run-e2e-tests.js
Outdated
|
||
async function run() { | ||
try { | ||
checkCredentials(); |
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'l make a similar comment here about my own anxiety and obscuring what's happening vs. hiding how it's happening. this doesn't give me much information about the credentials even though I assume it's checking something.
Another formulation might abstract the check and expose the things being checked…
async function run() {
const requiredENVs = [ 'E2EUSERNAME', 'E2EPASSWORD', 'E2E_MAILOSAUR_INBOX' ];
const missingENVs = requiredENVs.filter( name => ! process.env[name] || process.env[name] === '' );
if ( missingENVs.length ) {
throw new Error( `Missing non-empty ENV for: ${ missingENVs.join(', ')` );
}
…
}
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.
Agreed - see my response to your prior comment. Everything's been refactored inline. Also, I like the above as a much more concise way to check the required environment variables. See refactor here.
finally { | ||
process.exit(); | ||
} | ||
} |
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.
just because the entire function body is a try
maybe it's worth splitting? probably fine as-is, but feels a little silly having a big function body inside a try
async function run_unsafe() {
…
}
async function run() {
try {
run_unsafe();
} catch (err) {
console.error( err );
} finally {
process.exit()
}
}
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.
In this specific case, the entirety of the logic directly encapsulated within the try
block is evident at a glance, and further encapsulation in a wrapper method is unnecessary (and IMO adds an unnecessary level of indirection, and could be even be confusing). My preference here is to leave it as-is.
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.
Thanks for all the work you've put into this. I know from our conversations that you've put in far more on this than it may look just from the produced code.
Feel free to add your thoughts and disagree, but when I read this I look at it like systems code/run scripts and I feel different pressures than I would with normal application code. I care much more about knowing what is going on than I do about what should be going on, if that makes any sense. I want to see as little modularization and abstraction as is reasonable, compared to when I do want to abstract away details from normal code we write.
Put another way, I find more value in showing in a large function with separate "phases" or sections the actual CLI command and args than I do in delegating that work to separate functions, or even more so, to separate modules.
function run() {
// check required ENV vars
…
// start the Electron app
…
// start Chromedriver so it hooks into the app
…
// run the actual tests
…
}
there's extra noise here due to the node
/shell mismatch but I feel like we can abstract the concept of what we're doing (as suggesting in another comment I left) without hiding the specifics of what we're doing.
./node_modules/.bin/chromedriver --port=9515 --verbose > "$DRIVERLOG" 2>&1 & | ||
./node_modules/.bin/mocha test/tests/e2e.js | ||
osascript -e 'quit app "WordPress.com"' | ||
make e2e |
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.
should we just call npm run e2e
here instead of calling through make
?
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 think it might be nice to have the option of calling the tests either way. There's also some benefit to exposing/documenting critical workflow tooling in the makefile. In addition, all paths in the test script are absolute, which, combined with make
, gives the developer flexibility in how they are invoked. (e.g. tweaking something from within the Calypso submodule and invoking with make -f ./Makefile e2e
(tested and this works, btw)).
test/run/init_tests.js
Outdated
} ); | ||
tests.stdout.pipe( process.stdout ); | ||
tests.stderr.pipe( process.stderr ); | ||
} ); |
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.
given my comment about the process runner, this one almost seems like it would be fine inline in the main test runner module. it's ugly, but if we remove some of the variables here it's not so bad, especially since we remove a level of Promise
async function run() {
…
// run the tests
const testPath = path.join( __dirname, '../../test/tests/e2e.js' );
// by default should pipe stdout and stderr to this parent process
// will throw an error on exception
execSync( `npx mocha ${ testPath } --timeout 20000` );
}
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.
Great suggestion. Refactored inline with execSync
here. We have to add the additional stdio: 'inherit'
option so we can leverage streamed (and not buffered) output to the current shell, and it works great. 👍
1b5cc95
to
ed6b772
Compare
test/run/run-e2e-tests.js
Outdated
function initLogs( timestamp ) { | ||
const dir = path.join( PROJECT_DIR, 'test', 'logs', `${ timestamp }` ); | ||
|
||
if ( !existsSync( dir ) ) { |
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.
note that this introduces a race condition that may not protect us. the directory could be created between the call to exists
and the call to mkdir
probably simpler to just call mkdirSync
and handle failure if the directory already exists. it may only fail in windows.
in other words, for the same level of safety we can skip existsSync()
try {
mkdirSync( dir, { recursive: true } );
} catch (e) {
// pass
}
or do the more elegant/complete thing
try {
mkdirSync( dir, { recursive: true } );
} catch (e) {
if (!existsSync(dir)) {
console.error('Could not create log directory');
process.exit(1);
}
}
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.
According to the docs it may never throw an error if recursive: true
and we're not creating /
on Windows
Calling fs.mkdir() when path is a directory that exists results in an error only when recursive is false.
so in that case we may have the same safety as with existsSync()
without any further try/catch. if it fails the build crashes as it must.
…
mkdirSync(dir, {recursive: true });
…
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.
Thanks for double-checking the usage here. Simplified this in f4021b9
console.error( err ); | ||
} | ||
finally { | ||
process.exit(); |
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 probably worth a comment here explaining that this kills the child processes which otherwise might continue to run. looking at the code it's odd seeing finally { process.exit() }
as the last real line of execution. someone might come around and "improve" the code by removing 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.
... someone might come around and "improve" the code by removing it
😆Good point. Added comment in 87b55d5.
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 did not test this but from a code-audit perspective it looks quite good. Thanks for staying diligent on it and for being flexible to explore a few different approaches.
Description
This PR adds tooling to enable easier invocation of the end-to-end suite locally, and sets the stage for cross-platform invocation in the near future.
To-date, running tests locally has been a clumsy and cumbersome process in which developers had to cross-reference the existing Circle CI script to determine the steps required to invoke the test suite. This involved multiple manual steps:
Debugging and augmenting the test suite locally has been significantly hampered by the lack of tooling to enable faster and more efficient iteration. The goal of this utility is to significantly reduce this overhead such that invoking the tests only requires two steps:
make e2e
ornpm run e2e
At the end of teach test run (or when the suite errors or is interrupted via
CTRL+C
), all processes are automatically shut down. Automated process cleanup allows repeated tests runs with little to no manual intervention between each test run.To Test
make e2e
ornpm run e2e
Alternatives
Alternatives using
make
andbash
were explored, but ultimately did not function as desired.bash
in particular seems promising and might be worth re-visiting down the road.