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

console output helper in Cakefile for test case selection and parser generation statistics #5473

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 76 additions & 7 deletions Cakefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
fs = require 'fs'
os = require 'os'
path = require 'path'
{ performance } = require 'perf_hooks'
_ = require 'underscore'
{ spawn, exec, execSync } = require 'child_process'
CoffeeScript = require './lib/coffeescript'
helpers = require './lib/coffeescript/helpers'
{ setupConsole } = require './build-support/console'
{ PatternSet } = require './build-support/patterns'

# ANSI Terminal Colors.
bold = red = green = yellow = reset = ''
unless process.env.NODE_DISABLE_COLORS
USE_COLORS = process.stdout.hasColors?() and not process.env.NODE_DISABLE_COLORS
if USE_COLORS
bold = '\x1B[0;1m'
red = '\x1B[0;31m'
green = '\x1B[0;32m'
Expand All @@ -29,6 +33,12 @@ header = """
# Used in folder names like `docs/v1`.
majorVersion = parseInt CoffeeScript.VERSION.split('.')[0], 10

option '-l', '--level [LEVEL]', 'log level [debug < info < log(default) < warn < error]'

task = (name, description, action) ->
global.task name, description, ({level = 'log', ...opts} = {}) ->
setupConsole {level, useColors: USE_COLORS}
action {...opts}

# Log a message with a color.
log = (message, color, explanation) ->
Expand All @@ -53,13 +63,32 @@ run = (args, callback) ->
buildParser = ->
helpers.extend global, require 'util'
require 'jison'

startParserBuild = performance.now()

# Gather summary statistics about the grammar.
parser = require('./lib/coffeescript/grammar').parser
{symbols_, terminals_, productions_} = parser
countKeys = (obj) -> (Object.keys obj).length
numSyms = countKeys symbols_
numTerms = countKeys terminals_
numProds = countKeys productions_
console.info "parser created (#{numSyms} symbols, #{numTerms} terminals, #{numProds} productions)"

loadGrammar = performance.now()
console.info "loading grammar: #{loadGrammar - startParserBuild} ms"

# We don't need `moduleMain`, since the parser is unlikely to be run standalone.
parser = require('./lib/coffeescript/grammar').parser.generate(moduleMain: ->)
fs.writeFileSync 'lib/coffeescript/parser.js', parser
fs.writeFileSync 'lib/coffeescript/parser.js', parser.generate(moduleMain: ->)

parserBuildComplete = performance.now()
console.info "parser generation: #{parserBuildComplete - loadGrammar} ms"
console.info "full parser build time: #{parserBuildComplete - startParserBuild} ms"

buildExceptParser = (callback) ->
files = fs.readdirSync 'src'
files = ('src/' + file for file in files when file.match(/\.(lit)?coffee$/))
console.dir.debug {files}
run ['-c', '-o', 'lib/coffeescript'].concat(files), callback

build = (callback) ->
Expand Down Expand Up @@ -401,15 +430,24 @@ task 'bench', 'quick benchmark of compilation time', ->


# Run the CoffeeScript test suite.
runTests = (CoffeeScript) ->
runTests = (CoffeeScript, {filePatterns, negFilePatterns, descPatterns, negDescPatterns} = {}) ->
CoffeeScript.register() unless global.testingBrowser

filePatterns ?= PatternSet.empty()
negFilePatterns ?= PatternSet.empty {negated: yes}
descPatterns ?= PatternSet.empty()
negDescPatterns ?= PatternSet.empty {negated: yes}
console.dir.debug {filePatterns, negFilePatterns, descPatterns, negDescPatterns}

# These are attached to `global` so that they’re accessible from within
# `test/async.coffee`, which has an async-capable version of
# `global.test`.
global.currentFile = null
global.passedTests = 0
global.failures = []
global.filteredOut =
files: []
tests: []

global[name] = func for name, func of require 'assert'

Expand All @@ -429,9 +467,22 @@ runTests = (CoffeeScript) ->
error: err
description: description
source: fn.toString() if fn.toString?
onFilteredOut = (description, fn) ->
console.warn "test '#{description}' was filtered out by patterns"
filteredOut.tests.push
filename: global.currentFile
description: description
fn: fn
onFilteredFile = (file) ->
console.warn "file '#{file}' was filtered out by patterns"
filteredOut.files.push
filename: file

# Our test helper function for delimiting different test cases.
global.test = (description, fn) ->
unless (descPatterns.allows description) and (negDescPatterns.allows description)
onFilteredOut description, fn
return
try
fn.test = {description, currentFile}
result = fn.call(fn)
Expand All @@ -445,6 +496,7 @@ runTests = (CoffeeScript) ->
passedTests++
catch err
onFail description, fn, err
console.info "passed: #{description} in #{currentFile}"

helpers.extend global, require './test/support/helpers'

Expand Down Expand Up @@ -483,6 +535,9 @@ runTests = (CoffeeScript) ->

startTime = Date.now()
for file in files when helpers.isCoffee file
unless (filePatterns.allows file) and (negFilePatterns.allows file)
onFilteredFile file
continue
literate = helpers.isLiterate file
currentFile = filename = path.join 'test', file
code = fs.readFileSync filename
Expand All @@ -495,9 +550,23 @@ runTests = (CoffeeScript) ->
Promise.reject() if failures.length isnt 0


task 'test', 'run the CoffeeScript language test suite', ->
runTests(CoffeeScript).catch -> process.exit 1

option '-f', '--file [REGEXP*]', 'regexp patterns to positively match against test file paths'
option null, '--negFile [REGEXP*]', 'regexp patterns to negatively match against test file paths'
option '-d', '--desc [REGEXP*]', 'regexp patterns to positively match against test descriptions'
option null, '--negDesc [REGEXP*]', 'regexp patterns to negatively match against test descriptions'

task 'test', 'run the CoffeeScript language test suite', ({
file = [],
negFile = [],
desc = [],
negDesc = [],
} = {}) ->
testOptions =
filePatterns: new PatternSet file
negFilePatterns: new PatternSet negFile, {negated: yes}
descPatterns: new PatternSet desc
negDescPatterns: new PatternSet negDesc, {negated: yes}
runTests(CoffeeScript, testOptions).catch -> process.exit 1

task 'test:browser', 'run the test suite against the modern browser compiler in a headless browser', ->
# Create very simple web server to serve the two files we need.
Expand Down
95 changes: 95 additions & 0 deletions build-support/console.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{ Console } = require 'console'
process = require 'process'

exports.CakeConsole = class CakeConsole extends Console
@LEVELS: ['trace', 'debug', 'info', 'log', 'warn', 'error']
@validLevels: => "[#{(@LEVELS.map (l) -> "'#{l}'").join ', '}]"
@checkLevel: (level) =>
unless level in @LEVELS
throw new TypeError "argument '#{level}' was not a valid log level (should be: #{@validLevels()})"
level

constructor: ({level, ...opts} = {}) ->
super opts
@level = @constructor.checkLevel level ? 'log'

@getLevelNum: (l) => @LEVELS.indexOf @checkLevel l
curLevelNum: -> @constructor.getLevelNum @level
doesThisLevelApply: (l) -> @curLevelNum() <= @constructor.getLevelNum(l)

# Always log, regardless of level. This is for terminal output not intended to be configured by
# logging level.
unconditionalLog: (...args) ->
super.log ...args

# Define the named logging methods (.log(), .warn(), ...) by extracting them from the superclass.
trace: (...args) ->
if @doesThisLevelApply 'trace'
super ...args

debug: (...args) ->
if @doesThisLevelApply 'debug'
super ...args

info: (...args) ->
if @doesThisLevelApply 'info'
super ...args

log: (...args) ->
if @doesThisLevelApply 'log'
super ...args

warn: (...args) ->
if @doesThisLevelApply 'warn'
super ...args

error: (...args) ->
if @doesThisLevelApply 'error'
super ...args

# Call .dir(), but filtering by configured level.
dirLevel: (level, ...args) ->
if @doesThisLevelApply level
super.dir ...args

# We want to be able to call .dir() as normal, but we also want to be able to call .dir.log() to
# explicitly set the logging level for .dir().
Object.defineProperty @::, 'dir',
configurable: yes
get: ->
# By default, .dir() uses the 'log' level.
dir = (...args) -> @dirLevel 'log', ...args
Object.defineProperties dir, Object.fromEntries do => for k in @constructor.LEVELS
f = do (k) => (...args) => @dirLevel k, ...args
[k,
enumerable: yes
writable: yes
configurable: yes
value: Object.defineProperty f, 'name',
configurable: yes
value: k]
# We wouldn't normally have to set this, but Console does some wonky prototype munging:
# https://github.com/nodejs/node/blob/17fae65c72321659390c4cbcd9ddaf248accb953/lib/internal/console/constructor.js#L145-L147
set: (dir) -> # ignore

@stdio: ({
stdout = process.stdout,
stderr = process.stderr,
...opts,
} = {}) => new @ {
stdout,
stderr,
...opts
}


exports.setupConsole = ({level, useColors}) ->
if global.cakeConsole?
return global.cakeConsole

opts = {level}
unless useColors
opts.colorMode = no
global.console = global.cakeConsole = cakeConsole = CakeConsole.stdio opts
console.debug "log level = #{level}"
cakeConsole
18 changes: 18 additions & 0 deletions build-support/patterns.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
exports.PatternSet = class PatternSet
constructor: (patternStrings = [], {@negated = no} = {}) ->
@matchers = (new RegExp p for p in patternStrings when p isnt '')

isEmpty: -> @matchers.length is 0

iterMatchers: -> @matchers[Symbol.iterator]()

test_: (arg) -> @iterMatchers().some (m) -> m.exec arg

allows: (arg) ->
return yes if @isEmpty()
if @negated
not @test_ arg
else
@test_ arg

@empty: ({negated = no} = {}) => new @ [], {negated}
Loading