Skip to content

Commit

Permalink
control log levels and filter tests by file and description
Browse files Browse the repository at this point in the history
- add summary stats for parser generation
- add a perf counter for grammar build time
- add negative regex matching
- move Console to build-support
- move PatternSet to build-support
  • Loading branch information
cosmicexplorer committed Nov 24, 2024
1 parent 817c39a commit b913d1c
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 7 deletions.
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.info {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.info "test '#{description}' was filtered out by patterns"
filteredOut.tests.push
filename: global.currentFile
description: description
fn: fn
onFilteredFile = (file) ->
console.info "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*]', 'test file patterns to positively match'
option null, '--negFile [REGEXP*]', 'test file patterns to negatively match'
option '-d', '--desc [REGEXP*]', 'test description patterns to positively match'
option null, '--negDesc [REGEXP*]', 'test description patterns to negatively match'

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}

0 comments on commit b913d1c

Please sign in to comment.