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

simplified test file skipping (#4996) #5003

Merged
merged 10 commits into from
Mar 13, 2018

Conversation

rdeforest
Copy link
Contributor

This is a re-creation of PR #4998, which automatically closed because I deleted the branch it came from, and was itself a re-creation of PR #4997 without the extraneous history. Original description follows.

Per discussion on #4893 and #4997, managing exclusion of tests when newer features are required is currently non-obvious. The goal of this PR is to make maintenance of such exceptions easy and surprise-free.

The existing method was a hard-wired exception for the file 'async.coffee'. When I added async_iterators for #4893 I was surprised because I didn't know about the build:watch:harmony build target nor the hardwired exception in runTests. Now both the feature test and filename match are handled on the same lines near the top of Cakefile.

I included two currently unused pattern/feature pairs to demonstrate the intent and value of this approach. Both will likely be used for #4877 and #4893.

I didn't write a test for this change since it's a change to the test system.

After posting this I'm going to figure out how to remove the extra unrelated empty commits, but I wanted to get this out there before the weekend to follow through on my previous commitment.

@rdeforest
Copy link
Contributor Author

I started looking through other PRs and noticed that #4880 would benefit from this feature as well. The tests which need to be skipped would have to be in their own file though. Supposing it were called node9_regexps.coffee, the updated table would look something like

testFilesToSkip = [
  /async/           unless try new Function 'async ()   => {}'
  /async_iterators/ unless try new Function 'async () * => {}'
  /exponent_ops/    unless try new Function 'x = 3; x **= 2 ** 2; return x === 81'
  /node9/           unless process.version.match /^v9/
].filter _.identity

(I wonder why the GitHub coffee colorizer is confused by this code? Vim gets it right.)

@rdeforest
Copy link
Contributor Author

#4884 seems to want this too.

@rdeforest
Copy link
Contributor Author

rdeforest commented Mar 6, 2018

While I'm confident this patch is a step forward, I've been working on a more elegant approach.

My current thinking is similar to what I proposed on another thread, where each set of tests with the same requirements goes in its own directory. Instead of having a config file, index.coffee in each directory handles the should these tests run question.

Edit to add: #5007 is my more elegant solution and obsoletes this PR if it is accepted.

@zdenko
Copy link
Collaborator

zdenko commented Mar 7, 2018

I think this PR has better, easier solution for skipping specific tests. Organizing files in folders seem to much work. In most cases, there would be just one file (+ index.coffee) per folder.

This got me thinking if there could be an even better solution to skip tests. Instead of relying on the file name pattern (or folders) we could use custom identifiers. What if sometimes test needs to pass more then one check, e.g. async generators and object rest/spread?
So, here's my quick proposal (just brainstorming).

We could add an array of conditions per test function:

test "async as argument", ["async", "some_other_condition"], ->
  ok ->
    await winning()

(The array is placed after the description because some functions are quite long and I think it is more readable this way.)

The array of test conditions identifiers is similar as in this PR but contains conditions which passed the tests.

testFilesConditons = [
  /async/           if try new Function 'async ()   => {}'
  /async_iterators/ if try new Function 'async () * => {}'
  /exponent_ops/    if try new Function 'x = 3; x **= 2 ** 2; return x === 81'
].filter _.identity

featuresPresentFor = (cond) -> testFilesConditons.find (filePattern) -> cond.match filePattern

Then, in global.test, we check if the function has conditions and if any of them fails we skip the test.

# Our test helper function for delimiting different test cases.
global.test = (description, fnCond, fn = no) ->
  canTest = (conditions) ->
    passed = (yes for cond in conditions when featuresPresentFor(cond)?)
    passed.length is conditions.length

  try
    fn = fnCond if _.isFunction fnCond
    fn.test = {description, currentFile}
    # If the test has specific conditions (i.e. `async generators`) 
    # and doesn't pass, it will be skipped.
    if _.isArray(fnCond) and not canTest fnCond
      result = yes
    else
      result = fn.call(fn)
...

I tried this approach locally and it seems to be working.

@GeoffreyBooth
Copy link
Collaborator

I agree with @zdenko that I’d rather a Cakefile-based approach than a folder-based approach. It’s not that hard to edit the Cakefile, and we may have to for future oddball things like more attachments to global such as testAsync.

@zdenko I don’t think your suggestion will work. Use the tool n (npm install -g n) to switch to an older version of Node, like Node 6, and you’ll see why. Even if a particular function or block of code with new syntax never gets executed, for example if it’s inside an if supportsAsync block, the runtime still parses that never-to-be-executed code, and the parsing itself throws an error. That’s why we need to do these contortions to prevent unsupported syntax from ever being parsed, by preventing Node from ever loading the files in the first place; or by treating the experimental syntax as a string to be evaled.

Cakefile Outdated
@@ -29,6 +29,15 @@ header = """
# Used in folder names like `docs/v1`.
majorVersion = parseInt CoffeeScript.VERSION.split('.')[0], 10

# Patterns of names of test files which depend on currently absent features
testFilesToSkip = [
/async/ unless try new Function 'async () => {}'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn’t this be /^async$/ so that it doesn’t automatically include async_iterators as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My reasoning was that any file with 'async' in its name probably requires the async feature, but I don't mind being more specific if you prefer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should map feature detections to specific files, rather than relying on substrings in filenames. At quick glance it’s not obvious that /async/ will match both async.coffee and async_iterators.coffee, and so this is a potential bug in the future when someone adds another one that overlaps with filenames they didn’t intend. I think all you need to do is change these regexes into strings? And then change fileName.match to be an equality check.

Cakefile Outdated
/exponent_ops/ unless try new Function 'x = 3; x **= 2 ** 2; return x === 81'
].filter _.identity

featuresPresentFor = (fileName) ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you please explain the name of this variable? Why create this function as a separate variable at all, rather than an inline argument to filter below (similar to the old code)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking of it in terms of being a predicate.

if featuresPresentFor someFile then process someFile

But you're right that it looks weird when passed to filter. Perhaps it should be "fileCanRun"? I'm open to other suggestions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe just put the function inline next to filter? Then it doesn’t need a name.

@zdenko
Copy link
Collaborator

zdenko commented Mar 8, 2018

@GeoffreyBooth you're right of course. It won't work. In my experiment, I used simple test function which passed on Node 6.

@rdeforest
Copy link
Contributor Author

whoops. I should have run the tests before pushing... jussec

Cakefile Outdated
'async.coffee' unless try new Function 'async () => {}'
'async_iterators.coffee' unless try new Function 'async () * => {}'
'exponent_ops.coffee' unless try new Function 'x = 3; x **= 2 ** 2; return x === 81'
].filter _.identity
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the purpose of this is just to prevent undefined elements in the array? But such elements shouldn’t matter in filename not in testFilesToSkip. I know this is a bit nitpicky of an optimization, but I was scratching my head wondering what filter _.identity was doing 😄

Also please put a period at the end of your comment. The comments become the documentation in the annotated source, so they should be complete sentences.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When the list was treated as a list of RegExps to be String::match'd against, undefined was unwelcome in the list. Now it's fine to leave them in. This is why we have code reviews. :)

I also didn't realize the Cakefile was subjected to the same comment -> documentation treatment as the files in src/. I'm adjusting my comments to account for this. Update forthcoming...

Cakefile Outdated
# The `skipUnless` function adds file names to the `testFilesToSkip` array
# when the feature(s) the file(s) depend on are not available.
skipUnless = (code, names...) ->
if not (try new Function code)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be unless (try new Function code)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Cakefile Outdated
if not (try new Function code)
testFilesToSkip = testFilesToSkip.concat names

skipUnless 'async () => {}', 'async.coffee'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Technically, if async functions aren’t supported, we should be skipping async.coffee and async_iterators.coffee both. It’s irrelevant in this case because you have the second check below, but if we’re thinking of this code as being self-documenting—i.e., ”the files to skip when this test fails are…”—then the second argument should be ['async.coffee', 'async_iterators.coffee'].

Also I’m not sure how scalable this style is, when we add a test that isn’t < 60 characters or so, but we can worry about that when that day comes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's easy to re-factor for widely-applicable tests

asyncTestFiles = (files...) ->
  skipUnless 'async () = {}', files...

asyncTestFiles 'async.coffee', 'async_iterator.coffee'
asyncTestFiles 'some_other_async_test.coffee'

Or we could get really wild...

asyncTest = 'async () -> {}'
appendDotCoffee = (s) -> s + ".coffee"

asyncPrefixes = (s) -> skipUnless asyncTest, s.split(/\s+/).map(appendCoffee)...

asyncPrefixes """
   async
  async_iterators
  some_other_async_test
"""

Can you tell I <3 CoffeeScript? :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you tell I <3 CoffeeScript? :)

Indeed. 😄 We try to make the CoffeeScript codebase a “best practices” example of how to write CoffeeScript, so code golf isn’t exactly what we’re going for. 😄 I think something as simple as making the second argument an array is probably sufficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's my Perl roots showing. Sorry about that. :) As you said, we can cross that bridge when we come to it.

Cakefile Outdated

skipUnless 'async () => {}', 'async.coffee'
skipUnless 'async () * => {}', 'async_iterators.coffee'
skipUnless 'x = 3; x **= 2 ** 2; return x === 81', 'exponent_ops.coffee'
Copy link
Collaborator

Choose a reason for hiding this comment

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

@rdeforest If you don’t mind just changing this section so that the second parameters are arrays, and the first one is both async.coffee and async_iterators.coffee, I think this can be merged in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done and done. I also made the third test multi-line to serve as an example. I hope you agree it's an improvement.

@GeoffreyBooth
Copy link
Collaborator

Thanks @rdeforest!

@rdeforest rdeforest deleted the master-pending-4998 branch March 13, 2018 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants