Gradle: Basic Kotlin NodeJS tests runner
#KT-30531 Fixed #KT-30528 Fixed
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# test directories
|
||||
__tests__
|
||||
test
|
||||
tests
|
||||
powered-test
|
||||
|
||||
# asset directories
|
||||
docs
|
||||
doc
|
||||
website
|
||||
images
|
||||
assets
|
||||
|
||||
# examples
|
||||
example
|
||||
examples
|
||||
|
||||
# code coverage directories
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# build scripts
|
||||
Makefile
|
||||
Gulpfile.js
|
||||
Gruntfile.js
|
||||
|
||||
# configs
|
||||
appveyor.yml
|
||||
circle.yml
|
||||
codeship-services.yml
|
||||
codeship-steps.yml
|
||||
wercker.yml
|
||||
.tern-project
|
||||
.gitattributes
|
||||
.editorconfig
|
||||
.*ignore
|
||||
.eslintrc
|
||||
.jshintrc
|
||||
.flowconfig
|
||||
.documentup.json
|
||||
.yarn-metadata.json
|
||||
.travis.yml
|
||||
|
||||
# misc
|
||||
*.md
|
||||
@@ -0,0 +1,6 @@
|
||||
Kotlin/JS TeamCity tests results reporter.
|
||||
|
||||
### Usage
|
||||
|
||||
`yarn add @kotlin/js-tests-teamcity --dev`
|
||||
`yarn run kotlin-js-tests`
|
||||
@@ -0,0 +1,49 @@
|
||||
import com.moowork.gradle.node.yarn.YarnTask
|
||||
|
||||
plugins {
|
||||
id("base")
|
||||
id("com.moowork.node") version "1.2.0"
|
||||
}
|
||||
|
||||
node {
|
||||
version = "11.9.0"
|
||||
download = true
|
||||
nodeModulesDir = projectDir
|
||||
}
|
||||
|
||||
tasks {
|
||||
"yarn" {
|
||||
outputs.upToDateWhen {
|
||||
projectDir.resolve("node_modules").isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
create<YarnTask>("yarnBuild") {
|
||||
group = "build"
|
||||
|
||||
dependsOn("yarn")
|
||||
setWorkingDir(projectDir)
|
||||
args = listOf("build")
|
||||
|
||||
inputs.dir(projectDir.resolve("src"))
|
||||
outputs.file(projectDir.resolve("lib/kotlin-js-test.js"))
|
||||
}
|
||||
|
||||
create<Delete>("cleanYarn") {
|
||||
group = "build"
|
||||
|
||||
delete = setOf(
|
||||
projectDir.resolve("node_modules"),
|
||||
projectDir.resolve("lib"),
|
||||
projectDir.resolve(".rpt2_cache")
|
||||
)
|
||||
}
|
||||
|
||||
getByName("clean").dependsOn("cleanYarn")
|
||||
}
|
||||
|
||||
artifacts {
|
||||
add("archives", projectDir.resolve("lib/kotlin-js-test.js")) {
|
||||
builtBy("yarnBuild")
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import {TeamCityMessagesFlow} from "./src/TeamCityMessagesFlow";
|
||||
import {directRunner, KotlinTestRunner} from "./src/KotlinTestRunner";
|
||||
import {IgnoredTestSuitesReporting, runWithTeamCityReporter} from "./src/KotlinTestTeamCityReporter";
|
||||
import {CliArgsParser} from "./src/CliArgsParser";
|
||||
import {configureFiltering} from "./src/CliFiltertingConfiguration";
|
||||
import {hrTimer} from "./src/Timer";
|
||||
|
||||
const kotlin_test = require('kotlin-test');
|
||||
|
||||
const parser = new CliArgsParser({
|
||||
version: VERSION,
|
||||
bin: BIN,
|
||||
description: DESCRIPTION,
|
||||
usage: "[-t --tests] [-e --exclude] <module_name1>, <module_name2>, ..",
|
||||
args: {
|
||||
include: {
|
||||
keys: ['--tests', '--include'],
|
||||
help: "Tests to include. Example: MySuite.test1,MySuite.MySubSuite.*,*unix*,!*windows*",
|
||||
default: "*"
|
||||
|
||||
},
|
||||
exclude: {
|
||||
keys: ['--exclude'],
|
||||
help: "Tests to exclude. Example: MySuite.test1,MySuite.MySubSuite.*,*unix*"
|
||||
},
|
||||
ignoredTestSuites: {
|
||||
keys: ['--ignoredTestSuites'],
|
||||
help: "How to deal with ignored test suites",
|
||||
single: true,
|
||||
values: [
|
||||
IgnoredTestSuitesReporting.skip,
|
||||
IgnoredTestSuitesReporting.reportAsIgnoredTest,
|
||||
IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored
|
||||
],
|
||||
valuesHelp: [
|
||||
"don't report ignored test suites",
|
||||
"useful to speedup large ignored test suites",
|
||||
"will cause visiting all inner tests",
|
||||
],
|
||||
default: IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored
|
||||
}
|
||||
},
|
||||
freeArgsTitle: "module_name"
|
||||
});
|
||||
|
||||
const processArgs = process.argv.slice(2);
|
||||
const untypedArgs = parser.parse(processArgs);
|
||||
const args = {
|
||||
onIgnoredTestSuites: (untypedArgs.ignoredTestSuites
|
||||
|| IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored) as IgnoredTestSuitesReporting,
|
||||
include: untypedArgs.include as string[],
|
||||
exclude: untypedArgs.exclude as string[],
|
||||
};
|
||||
|
||||
const teamCity = new TeamCityMessagesFlow(null, (payload) => console.log(payload));
|
||||
|
||||
let runner: KotlinTestRunner = directRunner;
|
||||
runner = runWithTeamCityReporter(runner, args.onIgnoredTestSuites, teamCity, hrTimer);
|
||||
runner = configureFiltering(runner, args.include, args.exclude);
|
||||
|
||||
kotlin_test.setAdapter(runner);
|
||||
|
||||
untypedArgs.free.forEach((arg: string) => {
|
||||
require(arg);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
declare const DEBUG: boolean;
|
||||
declare const VERSION: string;
|
||||
declare const BIN: string;
|
||||
declare const DESCRIPTION: string;
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@kotlin/js-tests-teamcity",
|
||||
"version": "0.0.1",
|
||||
"description": "Simple Kotlin/JS tests runner with TeamCity reporter",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"bin": {
|
||||
"kotlin-js-tests": "lib/kotlin-js-test.js"
|
||||
},
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rimraf lib/* && rollup -c rollup.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^10.12.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.1.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"rollup": "^1.1.2",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-sourcemaps": "^0.4.2",
|
||||
"rollup-plugin-typescript2": "^0.19.2",
|
||||
"rollup-plugin-uglify": "^6.0.2",
|
||||
"typescript": "^3.3.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import {uglify} from "rollup-plugin-uglify";
|
||||
import nodeResolve from 'rollup-plugin-node-resolve';
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
|
||||
const pckg = require('./package.json');
|
||||
|
||||
export default [
|
||||
{
|
||||
input: './cli.ts',
|
||||
output: {
|
||||
file: 'lib/kotlin-js-test.js',
|
||||
format: 'cjs',
|
||||
banner: '#!/usr/bin/env node'
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
jsnext: true,
|
||||
main: true
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({
|
||||
tsconfig: "tsconfig.json"
|
||||
}),
|
||||
uglify({
|
||||
sourcemap: true,
|
||||
compress: {
|
||||
// hoist_funs: true,
|
||||
// hoist_vars: true,
|
||||
toplevel: true,
|
||||
unsafe: true,
|
||||
dead_code: true,
|
||||
global_defs: {
|
||||
DEBUG: false,
|
||||
VERSION: pckg.version,
|
||||
BIN: Object.keys(pckg.bin)[0],
|
||||
DESCRIPTION: pckg.description
|
||||
}
|
||||
},
|
||||
mangle: {
|
||||
properties: {
|
||||
keep_quoted: true,
|
||||
reserved: [
|
||||
"argv", "hrtime",
|
||||
"kotlin_test", "kotlin", "setAdapter", "setAssertHook_4duqou$",
|
||||
"suite", "test",
|
||||
"stack"
|
||||
]
|
||||
},
|
||||
toplevel: true,
|
||||
},
|
||||
// output: {
|
||||
// beautify: true
|
||||
// }
|
||||
}),
|
||||
// sourceMaps()
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,115 @@
|
||||
import {println, startsWith} from "./utils";
|
||||
|
||||
export type CliDescription = {
|
||||
version: string,
|
||||
bin: string,
|
||||
description: string,
|
||||
usage: string,
|
||||
args: {
|
||||
[k: string]: CliArgDescription,
|
||||
},
|
||||
freeArgsTitle: string
|
||||
}
|
||||
|
||||
export type CliArgValues = {
|
||||
[k: string]: string[] | string,
|
||||
free: string[]
|
||||
}
|
||||
|
||||
export type CliArgDescription = {
|
||||
keys: string[],
|
||||
help: string,
|
||||
values?: string[],
|
||||
valuesHelp?: string[],
|
||||
default?: string,
|
||||
single?: true
|
||||
}
|
||||
|
||||
export class CliArgsParser {
|
||||
constructor(private description: CliDescription) {
|
||||
}
|
||||
|
||||
printUsage() {
|
||||
const description = this.description;
|
||||
|
||||
println(`${description.bin} v${description.version} - ${description.description}`);
|
||||
println();
|
||||
println(`Usage: ${description.bin} ${description.usage}`);
|
||||
println();
|
||||
for (let key in description.args) {
|
||||
const data = description.args[key];
|
||||
println(' ' + data.keys.join(', '));
|
||||
const indent = ' ';
|
||||
println(`${indent}${data.help}`);
|
||||
if (data.values && data.valuesHelp) {
|
||||
println(`${indent}Possible values:`);
|
||||
for (let i = 0; i < data.values.length; i++) {
|
||||
const value = data.values[i];
|
||||
const help = data.valuesHelp[i];
|
||||
println(`${indent} - "${value}": ${help}`)
|
||||
}
|
||||
}
|
||||
if (data.default) println(`${indent}By default: ${data.default}`);
|
||||
println('')
|
||||
}
|
||||
}
|
||||
|
||||
badArgsExit(message: string) {
|
||||
println(message);
|
||||
println();
|
||||
this.printUsage();
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
parse(args: string[]): CliArgValues {
|
||||
const description = this.description;
|
||||
|
||||
const result: CliArgValues = {
|
||||
free: []
|
||||
};
|
||||
for (let key in description.args) {
|
||||
if (!description.args[key].single) {
|
||||
result[key] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// process all arguments from left to right
|
||||
args: while (args.length != 0) {
|
||||
const arg = args.shift() as string;
|
||||
|
||||
if (startsWith(arg, '--')) {
|
||||
for (let argName in description.args) {
|
||||
const argDescription = description.args[argName];
|
||||
if (argDescription.keys.indexOf(arg) != -1) {
|
||||
if (args.length == 0) {
|
||||
this.badArgsExit("Missed value after option " + arg);
|
||||
}
|
||||
|
||||
const value = args.shift() as string;
|
||||
if (argDescription.values && argDescription.values.indexOf(value) == -1) {
|
||||
this.badArgsExit("Unsupported value for option " + arg);
|
||||
}
|
||||
|
||||
if (argDescription.single) {
|
||||
result[argName] = value;
|
||||
} else {
|
||||
(result[argName] as string[]).push(value);
|
||||
}
|
||||
|
||||
continue args;
|
||||
}
|
||||
}
|
||||
|
||||
this.badArgsExit("Unknown option: " + arg);
|
||||
} else {
|
||||
result.free.push(arg)
|
||||
}
|
||||
}
|
||||
|
||||
if (result.free.length == 0) {
|
||||
this.badArgsExit(`At least one ${description.freeArgsTitle} should be provided`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {KotlinTestRunner} from "./KotlinTestRunner";
|
||||
import {
|
||||
allTest,
|
||||
CompositeTestFilter,
|
||||
KotlinTestsFilter,
|
||||
newKotlinTestsFilter,
|
||||
runWithFilter
|
||||
} from "./KotlinTestsFilter";
|
||||
import {flatMap, println, pushIfNotNull} from "./utils";
|
||||
|
||||
export function configureFiltering(
|
||||
runner: KotlinTestRunner,
|
||||
includeWildcards: string[],
|
||||
excludeWildcards: string[]
|
||||
): KotlinTestRunner {
|
||||
const include: KotlinTestsFilter[] = [];
|
||||
const exclude: KotlinTestsFilter[] = [];
|
||||
|
||||
function collectWildcards(
|
||||
value: string[],
|
||||
positive: KotlinTestsFilter[],
|
||||
negative: KotlinTestsFilter[]
|
||||
) {
|
||||
flatMap(value, (t: string) => t.split(','))
|
||||
.map(t => {
|
||||
if (t.length && t[0] == '!') {
|
||||
pushIfNotNull(negative, newKotlinTestsFilter(t.substring(1)))
|
||||
} else {
|
||||
pushIfNotNull(positive, newKotlinTestsFilter(t))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
collectWildcards(includeWildcards, include, exclude);
|
||||
collectWildcards(excludeWildcards, exclude, include);
|
||||
|
||||
if (include.length == 0 && exclude.length == 0) {
|
||||
return runner
|
||||
} else {
|
||||
if (include.length == 0) {
|
||||
include.push(allTest)
|
||||
}
|
||||
|
||||
const filter = new CompositeTestFilter(include, exclude);
|
||||
|
||||
if (DEBUG) {
|
||||
println(filter.toString());
|
||||
}
|
||||
|
||||
return runWithFilter(runner, filter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface KotlinTestRunner {
|
||||
suite(name: string, isIgnored: boolean, fn: () => void): void
|
||||
|
||||
test(name: string, isIgnored: boolean, fn: () => void): void
|
||||
}
|
||||
|
||||
export const directRunner: KotlinTestRunner = {
|
||||
suite(name: string, isIgnored: boolean, fn: () => void): void {
|
||||
if (!isIgnored) fn()
|
||||
},
|
||||
test(name: string, isIgnored: boolean, fn: () => void): void {
|
||||
if (!isIgnored) fn()
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import {KotlinTestRunner} from "./KotlinTestRunner";
|
||||
import {TeamCityMessageData, TeamCityMessagesFlow} from "./TeamCityMessagesFlow";
|
||||
import {Timer} from "./Timer";
|
||||
|
||||
const kotlin_test = require('kotlin-test');
|
||||
|
||||
// don't use enum as it is not minified by uglify
|
||||
export type IgnoredTestSuitesReporting
|
||||
= "skip" | "reportAsIgnoredTest" | "reportAllInnerTestsAsIgnored"
|
||||
export const IgnoredTestSuitesReporting: { [key: string]: IgnoredTestSuitesReporting } = {
|
||||
skip: "skip",
|
||||
reportAsIgnoredTest: "reportAsIgnoredTest",
|
||||
reportAllInnerTestsAsIgnored: "reportAllInnerTestsAsIgnored"
|
||||
};
|
||||
|
||||
// to reduce minified code size
|
||||
function withName(name: string, data?: TeamCityMessageData): TeamCityMessageData {
|
||||
data = data || {};
|
||||
data["name"] = name;
|
||||
return data
|
||||
}
|
||||
|
||||
export function runWithTeamCityReporter(
|
||||
runner: KotlinTestRunner,
|
||||
ignoredTestSuites: IgnoredTestSuitesReporting,
|
||||
teamCity: TeamCityMessagesFlow,
|
||||
timer: Timer<any> | null
|
||||
): KotlinTestRunner {
|
||||
let inIgnoredSuite = false;
|
||||
let currentAssertionResult: { expected: any, actual: any } | null = null;
|
||||
|
||||
kotlin_test.kotlin.test.setAssertHook_4duqou$(function (assertionResult: { expected: any, actual: any }) {
|
||||
currentAssertionResult = assertionResult;
|
||||
});
|
||||
|
||||
return {
|
||||
suite: function (name: string, isIgnored: boolean, fn: () => void) {
|
||||
if (isIgnored) {
|
||||
if (ignoredTestSuites == IgnoredTestSuitesReporting.skip) return;
|
||||
else if (ignoredTestSuites == IgnoredTestSuitesReporting.reportAsIgnoredTest) {
|
||||
teamCity.sendMessage('testIgnored', withName(name, {"suite": true}));
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
teamCity.sendMessage('testSuiteStarted', withName(name));
|
||||
|
||||
// noinspection UnnecessaryLocalVariableJS
|
||||
const alreadyInIgnoredSuite = inIgnoredSuite;
|
||||
if (!alreadyInIgnoredSuite && isIgnored) {
|
||||
inIgnoredSuite = true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isIgnored && ignoredTestSuites == IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored) {
|
||||
fn();
|
||||
} else {
|
||||
runner.suite(name, isIgnored, fn)
|
||||
}
|
||||
} finally {
|
||||
if (isIgnored && !alreadyInIgnoredSuite) {
|
||||
inIgnoredSuite = false;
|
||||
}
|
||||
|
||||
const data: TeamCityMessageData = withName(name);
|
||||
|
||||
// extension only for Gradle
|
||||
if (isIgnored) data["ignored"] = true;
|
||||
|
||||
teamCity.sendMessage('testSuiteFinished', data);
|
||||
}
|
||||
},
|
||||
test: function (name: string, isIgnored: boolean, fn: () => void) {
|
||||
if (inIgnoredSuite || isIgnored) {
|
||||
teamCity.sendMessage('testIgnored', withName(name));
|
||||
} else {
|
||||
const startTime = timer ? timer.start() : null;
|
||||
teamCity.sendMessage('testStarted', withName(name));
|
||||
try {
|
||||
runner.test(name, isIgnored, fn);
|
||||
} catch (e) {
|
||||
const data: TeamCityMessageData = withName(name, {
|
||||
"message": e.message,
|
||||
"details": e.stack
|
||||
});
|
||||
|
||||
if (currentAssertionResult) {
|
||||
data["type"] = 'comparisonFailure';
|
||||
data["expected"] = currentAssertionResult.expected;
|
||||
data["actual"] = currentAssertionResult.actual;
|
||||
}
|
||||
teamCity.sendMessage('testFailed', data);
|
||||
} finally {
|
||||
currentAssertionResult = null;
|
||||
const data: TeamCityMessageData = withName(name);
|
||||
if (startTime) {
|
||||
data["duration"] = timer!!.end(startTime); // ns to ms
|
||||
}
|
||||
teamCity.sendMessage('testFinished', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import {escapeRegExp, startsWith, trim} from "./utils";
|
||||
import {KotlinTestRunner} from "./KotlinTestRunner";
|
||||
|
||||
export interface KotlinTestsFilter {
|
||||
mayContainTestsFromSuite(fqn: string): boolean;
|
||||
|
||||
containsTest(fqn: string): boolean;
|
||||
}
|
||||
|
||||
export function runWithFilter(
|
||||
runner: KotlinTestRunner,
|
||||
filter: KotlinTestsFilter,
|
||||
): KotlinTestRunner {
|
||||
let path: string[] = [];
|
||||
|
||||
function pathString() {
|
||||
// skip root
|
||||
return path.slice(1).join('.')
|
||||
}
|
||||
|
||||
return {
|
||||
suite: function (name: string, isIgnored: boolean, fn: () => void) {
|
||||
path.push(name);
|
||||
|
||||
try {
|
||||
if (path.length > 1 && !filter.mayContainTestsFromSuite(pathString())) return;
|
||||
|
||||
runner.suite(name, isIgnored, fn);
|
||||
} finally {
|
||||
path.pop()
|
||||
}
|
||||
},
|
||||
|
||||
test: function (name: string, isIgnored: boolean, fn: () => void) {
|
||||
path.push(name);
|
||||
|
||||
try {
|
||||
if (!filter.containsTest(pathString())) return;
|
||||
|
||||
runner.test(name, isIgnored, fn);
|
||||
} finally {
|
||||
path.pop()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function newKotlinTestsFilter(wildcard: string | null): KotlinTestsFilter | null {
|
||||
if (wildcard == null) return null;
|
||||
wildcard = trim(wildcard);
|
||||
wildcard = wildcard.replace(/\*+/, '*'); // ** => *
|
||||
if (wildcard.length == 0) return null;
|
||||
else if (wildcard == '*') return allTest;
|
||||
else if (wildcard.indexOf('*') == -1) return new ExactFilter(wildcard);
|
||||
else if (startsWith(wildcard, '*')) return new RegExpKotlinTestsFilter(wildcard);
|
||||
else {
|
||||
// optimize for cases like "Something*", "Something*a*b" and so on.
|
||||
// by adding explicit prefix matcher to not visit unneeded suites
|
||||
// (RegExpKotlinTestsFilter doesn't support suites matching)
|
||||
const [prefix, rest] = wildcard.split('*', 2);
|
||||
return new StartsWithFilter(prefix, rest ? new RegExpKotlinTestsFilter(wildcard) : null)
|
||||
}
|
||||
}
|
||||
|
||||
export const allTest = new class implements KotlinTestsFilter {
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export class StartsWithFilter implements KotlinTestsFilter {
|
||||
constructor(
|
||||
public readonly prefix: string,
|
||||
public readonly filter: RegExpKotlinTestsFilter | null
|
||||
) {
|
||||
}
|
||||
|
||||
isPrefixMatched(fqn: string): boolean {
|
||||
return startsWith(fqn + ".", this.prefix);
|
||||
}
|
||||
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
return this.isPrefixMatched(fqn);
|
||||
}
|
||||
|
||||
containsAllTestsFromSuite(fqn: string): boolean {
|
||||
return this.filter == null && this.isPrefixMatched(fqn);
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
return startsWith(fqn, this.prefix)
|
||||
&& (this.filter == null || this.filter.containsTest(fqn));
|
||||
}
|
||||
}
|
||||
|
||||
export class ExactFilter implements KotlinTestsFilter {
|
||||
constructor(public fqn: string) {
|
||||
}
|
||||
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
return startsWith(fqn, this.fqn);
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
return fqn === this.fqn;
|
||||
}
|
||||
}
|
||||
|
||||
export class RegExpKotlinTestsFilter implements KotlinTestsFilter {
|
||||
public readonly regexp: RegExp;
|
||||
|
||||
constructor(wildcard: string) {
|
||||
this.regexp = RegExp(wildcard
|
||||
.split('*')
|
||||
.map(it => escapeRegExp(it))
|
||||
.join('.*'));
|
||||
}
|
||||
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
return this.regexp!.test(fqn)
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.regexp.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export class CompositeTestFilter implements KotlinTestsFilter {
|
||||
private readonly excludePrefix: StartsWithFilter[] = [];
|
||||
|
||||
constructor(
|
||||
public include: KotlinTestsFilter[],
|
||||
public exclude: KotlinTestsFilter[]
|
||||
) {
|
||||
this.exclude.forEach(it => {
|
||||
if (it instanceof StartsWithFilter && it.filter == null)
|
||||
this.excludePrefix.push(it)
|
||||
})
|
||||
}
|
||||
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
for (const excl of this.excludePrefix) {
|
||||
if (excl.containsAllTestsFromSuite(fqn)) return false
|
||||
}
|
||||
for (const incl of this.include) {
|
||||
if (incl.mayContainTestsFromSuite(fqn)) return true
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
for (const excl of this.exclude) {
|
||||
if (excl.containsTest(fqn)) return false
|
||||
}
|
||||
for (const incl of this.include) {
|
||||
if (incl.containsTest(fqn)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
allTest,
|
||||
CompositeTestFilter,
|
||||
ExactFilter,
|
||||
RegExpKotlinTestsFilter,
|
||||
StartsWithFilter
|
||||
} from "./KotlinTestsFilter";
|
||||
|
||||
declare const DEBUG: boolean;
|
||||
|
||||
if (DEBUG) {
|
||||
allTest.toString = function () {
|
||||
return "(ALL)"
|
||||
};
|
||||
|
||||
StartsWithFilter.prototype.toString = function (): string {
|
||||
return "(STARTS WITH " + this.prefix + (this.filter ? (" AND " + this.filter) : "") + ")"
|
||||
};
|
||||
|
||||
RegExpKotlinTestsFilter.prototype.toString = function (): string {
|
||||
return "(REGEXP " + this.regexp + ")"
|
||||
};
|
||||
|
||||
ExactFilter.prototype.toString = function (): string {
|
||||
return "(EXACT " + this.fqn + ")"
|
||||
};
|
||||
|
||||
CompositeTestFilter.prototype.toString = function (): string {
|
||||
return this.include.map(it => it.toString()).join(" OR ")
|
||||
+ "\n NOT (" + this.exclude.map(it => it.toString()).join(" OR ") + ")"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {dateTimeWithoutTimeZone, newFlowId, tcEscape} from "./utils"
|
||||
|
||||
export type TeamCityMessageData = { [key: string]: any }
|
||||
|
||||
export class TeamCityMessagesFlow {
|
||||
public readonly id: number;
|
||||
|
||||
constructor(id: number | null, private readonly send: (payload: string) => void) {
|
||||
this.id = id || newFlowId()
|
||||
}
|
||||
|
||||
sendMessage(type: string, args: TeamCityMessageData) {
|
||||
args['flowId'] = this.id;
|
||||
args['timestamp'] = dateTimeWithoutTimeZone();
|
||||
|
||||
const serializedArgs = Object
|
||||
.keys(args)
|
||||
.map((key) => `${key}='${tcEscape(args[key])}'`)
|
||||
.join(' ');
|
||||
|
||||
this.send(`##teamcity[${type} ${serializedArgs}]`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface Timer<T> {
|
||||
start(): T
|
||||
|
||||
end(start: T): number
|
||||
}
|
||||
|
||||
export const hrTimer: Timer<[number, number]> = {
|
||||
start(): [number, number] {
|
||||
return process.hrtime();
|
||||
},
|
||||
end(start: [number, number]): number {
|
||||
const elapsedHr = process.hrtime(start);
|
||||
return elapsedHr[0] + (elapsedHr[1] / 1e6); // ns to ms
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
/**
|
||||
* from teamcity-service-messages
|
||||
* Copyright (c) 2013 Aaron Forsander
|
||||
*
|
||||
* Escape string for TeamCity output.
|
||||
* @see https://confluence.jetbrains.com/display/TCD65/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-servMsgsServiceMessages
|
||||
*/
|
||||
export function tcEscape(str: string): string {
|
||||
if (!str) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return str
|
||||
.toString()
|
||||
.replace(/\|/g, '||')
|
||||
.replace(/\n/g, '|n')
|
||||
.replace(/\r/g, '|r')
|
||||
.replace(/\[/g, '|[')
|
||||
.replace(/\]/g, '|]')
|
||||
.replace(/\u0085/g, '|x') // next line
|
||||
.replace(/\u2028/g, '|l') // line separator
|
||||
.replace(/\u2029/g, '|p') // paragraph separator
|
||||
.replace(/'/g, '|\'');
|
||||
}
|
||||
|
||||
/**
|
||||
* From teamcity-service-messages.
|
||||
* Copyright 2013 Aaron Forsander
|
||||
*/
|
||||
export function newFlowId(): number {
|
||||
return Math.floor(Math.random() * (1e10 - 1e6 + 1)) + 1e6;
|
||||
}
|
||||
|
||||
/**
|
||||
* From teamcity-service-messages.
|
||||
* Copyright 2013 Aaron Forsander
|
||||
*/
|
||||
export function dateTimeWithoutTimeZone(): string {
|
||||
// TeamCity not fully support ISO 8601 (see TW-36173) so we need to cut off 'Z' at the end.
|
||||
return new Date().toISOString().slice(0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* From lodash.
|
||||
* Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
*/
|
||||
export function startsWith(string: string, target: string) {
|
||||
return string.slice(0, target.length) == target;
|
||||
}
|
||||
|
||||
/**
|
||||
* From lodash.
|
||||
* Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
*/
|
||||
export function trim(str: string): string {
|
||||
return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to match `RegExp`
|
||||
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
|
||||
*/
|
||||
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g,
|
||||
reHasRegExpChar = RegExp(reRegExpChar.source);
|
||||
|
||||
/**
|
||||
* Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+",
|
||||
* "?", "(", ")", "[", "]", "{", "}", and "|" in `string`.
|
||||
*
|
||||
* From lodash.
|
||||
* Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
*/
|
||||
export function escapeRegExp(string: string) {
|
||||
return (string && reHasRegExpChar.test(string))
|
||||
? string.replace(reRegExpChar, '\\$&')
|
||||
: string;
|
||||
}
|
||||
|
||||
export function pushIfNotNull<T>(list: T[], value: T) {
|
||||
if (value !== null) list.push(value)
|
||||
}
|
||||
|
||||
export function flatMap<T>(arr: T[], f: (item: T) => T[]): T[] {
|
||||
const result: T[] = [];
|
||||
arr.forEach(item => {
|
||||
f(item).forEach(x => {
|
||||
result.push(x)
|
||||
})
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function println(message ?: string) {
|
||||
console.log(message)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "ESNext",
|
||||
// "declaration": true,
|
||||
// "outDir": "./lib",
|
||||
"strict": true,
|
||||
// "sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"inlineSourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"**/*Debug.ts"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user