testament: no sqlite or re dependencies anymore

This commit is contained in:
Araq 2017-10-25 16:27:46 +02:00
commit a5f1abc5ca
3 changed files with 116 additions and 268 deletions

View file

@ -1,56 +1,16 @@
# #
# #
# The Nim Tester # The Nim Tester
# (c) Copyright 2015 Andreas Rumpf # (c) Copyright 2017 Andreas Rumpf
# #
# Look at license.txt for more info. # Look at license.txt for more info.
# All rights reserved. # All rights reserved.
import strutils, db_sqlite, os, osproc import strutils, os, osproc, json
var db: DbConn
proc createDb() =
db.exec(sql"""
create table if not exists Machine(
id integer primary key,
name varchar(100) not null,
os varchar(20) not null,
cpu varchar(20) not null
);""")
db.exec(sql"""
create table if not exists [Commit](
id integer primary key,
hash varchar(256) not null,
branch varchar(50) not null
);""")
db.exec(sql"""
create table if not exists TestResult(
id integer primary key,
name varchar(100) not null,
category varchar(100) not null,
target varchar(20) not null,
action varchar(10) not null,
result varchar(30) not null,
[commit] int not null,
machine int not null,
expected varchar(10000) not null,
given varchar(10000) not null,
created timestamp not null default (DATETIME('now')),
foreign key ([commit]) references [commit](id),
foreign key (machine) references machine(id)
);""")
#db.exec(sql"""
# --create unique index if not exists TsstNameIx on TestResult(name);
# """, [])
type type
MachineId* = distinct int64 MachineId* = distinct string
CommitId = distinct int64 CommitId = distinct string
proc `$`*(id: MachineId): string {.borrow.} proc `$`*(id: MachineId): string {.borrow.}
proc `$`(id: CommitId): string {.borrow.} proc `$`(id: CommitId): string {.borrow.}
@ -58,11 +18,12 @@ proc `$`(id: CommitId): string {.borrow.}
var var
thisMachine: MachineId thisMachine: MachineId
thisCommit: CommitId thisCommit: CommitId
thisBranch: string
{.experimental.} {.experimental.}
proc `()`(cmd: string{lit}): string = cmd.execProcess.string.strip proc `()`(cmd: string{lit}): string = cmd.execProcess.string.strip
proc getMachine*(db: DbConn): MachineId = proc getMachine*(): MachineId =
var name = "hostname"() var name = "hostname"()
if name.len == 0: if name.len == 0:
name = when defined(posix): getenv"HOSTNAME".string name = when defined(posix): getenv"HOSTNAME".string
@ -70,54 +31,44 @@ proc getMachine*(db: DbConn): MachineId =
if name.len == 0: if name.len == 0:
quit "cannot determine the machine name" quit "cannot determine the machine name"
let id = db.getValue(sql"select id from Machine where name = ?", name) result = MachineId(name)
if id.len > 0:
result = id.parseInt.MachineId
else:
result = db.insertId(sql"insert into Machine(name, os, cpu) values (?,?,?)",
name, system.hostOS, system.hostCPU).MachineId
proc getCommit(db: DbConn): CommitId = proc getCommit(): CommitId =
const commLen = "commit ".len const commLen = "commit ".len
let hash = "git log -n 1"()[commLen..commLen+10] let hash = "git log -n 1"()[commLen..commLen+10]
let branch = "git symbolic-ref --short HEAD"() thisBranch = "git symbolic-ref --short HEAD"()
if hash.len == 0 or branch.len == 0: quit "cannot determine git HEAD" if hash.len == 0 or thisBranch.len == 0: quit "cannot determine git HEAD"
result = CommitId(hash)
let id = db.getValue(sql"select id from [Commit] where hash = ? and branch = ?", var
hash, branch) results: File
if id.len > 0: currentCategory: string
result = id.parseInt.CommitId entries: int
else:
result = db.insertId(sql"insert into [Commit](hash, branch) values (?, ?)",
hash, branch).CommitId
proc writeTestResult*(name, category, target, proc writeTestResult*(name, category, target,
action, result, expected, given: string) = action, result, expected, given: string) =
let id = db.getValue(sql"""select id from TestResult createDir("testresults")
where name = ? and category = ? and target = ? and if currentCategory != category:
machine = ? and [commit] = ?""", if currentCategory.len > 0:
name, category, target, results.writeLine("]")
thisMachine, thisCommit) close(results)
if id.len > 0: currentCategory = category
db.exec(sql"""update TestResult results = open("testresults" / category.addFileExt"json", fmWrite)
set action = ?, result = ?, expected = ?, given = ? results.writeLine("[")
where id = ?""", action, result, expected, given, id) entries = 0
else:
db.exec(sql"""insert into TestResult(name, category, target, let jentry = %*{"name": name, "category": category, "target": target,
action, "action": action, "result": result, "expected": expected, "given": given,
result, expected, given, "machine": thisMachine.string, "commit": thisCommit.string, "branch": thisBranch}
[commit], machine) if entries > 0:
values (?,?,?,?,?,?,?,?,?) """, name, category, target, results.writeLine(",")
action, results.write($jentry)
result, expected, given, inc entries
thisCommit, thisMachine)
proc open*() = proc open*() =
let dbFile = if existsEnv("TRAVIS") or existsEnv("APPVEYOR"): ":memory:" else: "testament.db" thisMachine = getMachine()
db = open(connection=dbFile, user="testament", password="", thisCommit = getCommit()
database="testament")
createDb()
thisMachine = getMachine(db)
thisCommit = getCommit(db)
proc close*() = close(db) proc close*() =
results.writeLine("]")
close(results)

View file

@ -1,7 +1,7 @@
# #
# #
# Nim Tester # Nim Tester
# (c) Copyright 2015 Andreas Rumpf # (c) Copyright 2017 Andreas Rumpf
# #
# See the file "copying.txt", included in this # See the file "copying.txt", included in this
# distribution, for details about the copyright. # distribution, for details about the copyright.
@ -9,20 +9,20 @@
## HTML generator for the tester. ## HTML generator for the tester.
import db_sqlite, cgi, backend, strutils, json import cgi, backend, strutils, json, os
import "testamenthtml.templ" import "testamenthtml.templ"
proc generateTestRunTabListItemPartial(outfile: File, testRunRow: Row, firstRow = false) = proc generateTestRunTabListItemPartial(outfile: File, testRunRow: JsonNode, firstRow = false) =
let let
# The first tab gets the bootstrap class for a selected tab # The first tab gets the bootstrap class for a selected tab
firstTabActiveClass = if firstRow: "active" firstTabActiveClass = if firstRow: "active"
else: "" else: ""
commitId = testRunRow[0] commitId = htmlQuote testRunRow["commit"].str
hash = htmlQuote(testRunRow[1]) hash = htmlQuote(testRunRow["commit"].str)
branch = htmlQuote(testRunRow[2]) branch = htmlQuote(testRunRow["branch"].str)
machineId = testRunRow[3] machineId = htmlQuote testRunRow["machine"].str
machineName = htmlQuote(testRunRow[4]) machineName = htmlQuote(testRunRow["machine"].str)
outfile.generateHtmlTabListItem( outfile.generateHtmlTabListItem(
firstTabActiveClass, firstTabActiveClass,
@ -33,17 +33,17 @@ proc generateTestRunTabListItemPartial(outfile: File, testRunRow: Row, firstRow
machineName machineName
) )
proc generateTestResultPanelPartial(outfile: File, testResultRow: Row, onlyFailing = false) = proc generateTestResultPanelPartial(outfile: File, testResultRow: JsonNode, onlyFailing = false) =
let let
trId = testResultRow[0] trId = htmlQuote(testResultRow["category"].str & "_" & testResultRow["name"].str)
name = testResultRow[1].htmlQuote() name = testResultRow["name"].str.htmlQuote()
category = testResultRow[2].htmlQuote() category = testResultRow["category"].str.htmlQuote()
target = testResultRow[3].htmlQuote() target = testResultRow["target"].str.htmlQuote()
action = testResultRow[4].htmlQuote() action = testResultRow["action"].str.htmlQuote()
result = testResultRow[5] result = htmlQuote testResultRow["result"].str
expected = testResultRow[6] expected = htmlQuote testResultRow["expected"].str
gotten = testResultRow[7] gotten = htmlQuote testResultRow["given"].str
timestamp = testResultRow[8] timestamp = "unknown"
var panelCtxClass, textCtxClass, bgCtxClass, resultSign, resultDescription: string var panelCtxClass, textCtxClass, bgCtxClass, resultSign, resultDescription: string
case result case result
of "reSuccess": of "reSuccess":
@ -84,92 +84,65 @@ proc generateTestResultPanelPartial(outfile: File, testResultRow: Row, onlyFaili
) )
outfile.generateHtmlTestresultPanelEnd() outfile.generateHtmlTestresultPanelEnd()
proc generateTestResultsPanelGroupPartial(outfile: File, db: DbConn, commitid, machineid: string, onlyFailing = false) = type
const testResultsSelect = sql""" AllTests = object
SELECT [tr].[id] data: JSonNode
, [tr].[name] totalCount, successCount, ignoredCount, failedCount: int
, [tr].[category] successPercentage, ignoredPercentage, failedPercentage: BiggestFloat
, [tr].[target]
, [tr].[action] proc allTestResults(): AllTests =
, [tr].[result] result.data = newJArray()
, [tr].[expected] for file in os.walkFiles("testresults/*.json"):
, [tr].[given] let data = parseFile(file)
, [tr].[created] if data.kind != JArray:
FROM [TestResult] AS [tr] echo "[ERROR] ignoring json file that is not an array: ", file
WHERE [tr].[commit] = ? else:
AND [tr].[machine] = ?""" for elem in data:
for testresultRow in db.rows(testResultsSelect, commitid, machineid): result.data.add elem
let state = elem["result"]
if state.contains("reSuccess"): inc result.successCount
elif state.contains("reIgnored"): inc result.ignoredCount
result.totalCount = result.data.len
result.successPercentage = 100 * (result.successCount.toBiggestFloat() / result.totalCount.toBiggestFloat())
result.ignoredPercentage = 100 * (result.ignoredCount.toBiggestFloat() / result.totalCount.toBiggestFloat())
result.failedCount = result.totalCount - result.successCount - result.ignoredCount
result.failedPercentage = 100 * (result.failedCount.toBiggestFloat() / result.totalCount.toBiggestFloat())
proc generateTestResultsPanelGroupPartial(outfile: File, allResults: JsonNode, onlyFailing = false) =
for testresultRow in allResults:
generateTestResultPanelPartial(outfile, testresultRow, onlyFailing) generateTestResultPanelPartial(outfile, testresultRow, onlyFailing)
proc generateTestRunTabContentPartial(outfile: File, db: DbConn, testRunRow: Row, onlyFailing = false, firstRow = false) = proc generateTestRunTabContentPartial(outfile: File, allResults: AllTests, testRunRow: JsonNode, onlyFailing = false, firstRow = false) =
let let
# The first tab gets the bootstrap classes for a selected and displaying tab content # The first tab gets the bootstrap classes for a selected and displaying tab content
firstTabActiveClass = if firstRow: " in active" firstTabActiveClass = if firstRow: " in active"
else: "" else: ""
commitId = testRunRow[0] commitId = htmlQuote testRunRow["commit"].str
hash = htmlQuote(testRunRow[1]) hash = htmlQuote(testRunRow["hash"].str)
branch = htmlQuote(testRunRow[2]) branch = htmlQuote(testRunRow["branch"].str)
machineId = testRunRow[3] machineId = htmlQuote testRunRow["machine"].str
machineName = htmlQuote(testRunRow[4]) machineName = htmlQuote(testRunRow["machine"].str)
os = htmlQuote(testRunRow[5]) os = htmlQuote("unknown_os")
cpu = htmlQuote(testRunRow[6]) cpu = htmlQuote("unknown_cpu")
const
totalClause = """
SELECT COUNT(*)
FROM [TestResult] AS [tr]
WHERE [tr].[commit] = ?
AND [tr].[machine] = ?"""
successClause = totalClause & "\L" & """
AND [tr].[result] LIKE 'reSuccess'"""
ignoredClause = totalClause & "\L" & """
AND [tr].[result] LIKE 'reIgnored'"""
let
totalCount = db.getValue(sql(totalClause), commitId, machineId).parseBiggestInt()
successCount = db.getValue(sql(successClause), commitId, machineId).parseBiggestInt()
successPercentage = 100 * (successCount.toBiggestFloat() / totalCount.toBiggestFloat())
ignoredCount = db.getValue(sql(ignoredClause), commitId, machineId).parseBiggestInt()
ignoredPercentage = 100 * (ignoredCount.toBiggestFloat() / totalCount.toBiggestFloat())
failedCount = totalCount - successCount - ignoredCount
failedPercentage = 100 * (failedCount.toBiggestFloat() / totalCount.toBiggestFloat())
outfile.generateHtmlTabPageBegin( outfile.generateHtmlTabPageBegin(
firstTabActiveClass, commitId, firstTabActiveClass, commitId,
machineId, branch, hash, machineName, os, cpu, machineId, branch, hash, machineName, os, cpu,
totalCount, allResults.totalCount,
successCount, formatBiggestFloat(successPercentage, ffDecimal, 2) & "%", allResults.successCount, formatBiggestFloat(allResults.successPercentage, ffDecimal, 2) & "%",
ignoredCount, formatBiggestFloat(ignoredPercentage, ffDecimal, 2) & "%", allResults.ignoredCount, formatBiggestFloat(allResults.ignoredPercentage, ffDecimal, 2) & "%",
failedCount, formatBiggestFloat(failedPercentage, ffDecimal, 2) & "%" allResults.failedCount, formatBiggestFloat(allResults.failedPercentage, ffDecimal, 2) & "%"
) )
generateTestResultsPanelGroupPartial(outfile, db, commitId, machineId, onlyFailing) generateTestResultsPanelGroupPartial(outfile, allResults.data, onlyFailing)
outfile.generateHtmlTabPageEnd() outfile.generateHtmlTabPageEnd()
proc generateTestRunsHtmlPartial(outfile: File, db: DbConn, onlyFailing = false) = proc generateTestRunsHtmlPartial(outfile: File, allResults: AllTests, onlyFailing = false) =
# Select a cross-join of Commits and Machines ensuring that the selected combination
# contains testresults
const testrunSelect = sql"""
SELECT [c].[id] AS [CommitId]
, [c].[hash] as [Hash]
, [c].[branch] As [Branch]
, [m].[id] AS [MachineId]
, [m].[name] AS [MachineName]
, [m].[os] AS [OS]
, [m].[cpu] AS [CPU]
FROM [Commit] AS [c], [Machine] AS [m]
WHERE (
SELECT COUNT(*)
FROM [TestResult] AS [tr]
WHERE [tr].[commit] = [c].[id]
AND [tr].[machine] = [m].[id]
) > 0
ORDER BY [c].[id] DESC
"""
# Iterating the results twice, get entire result set in one go # Iterating the results twice, get entire result set in one go
var testRunRowSeq = db.getAllRows(testrunSelect)
outfile.generateHtmlTabListBegin() outfile.generateHtmlTabListBegin()
var firstRow = true var firstRow = true
for testRunRow in testRunRowSeq: for testRunRow in allResults.data:
generateTestRunTabListItemPartial(outfile, testRunRow, firstRow) generateTestRunTabListItemPartial(outfile, testRunRow, firstRow)
if firstRow: if firstRow:
firstRow = false firstRow = false
@ -177,96 +150,20 @@ ORDER BY [c].[id] DESC
outfile.generateHtmlTabContentsBegin() outfile.generateHtmlTabContentsBegin()
firstRow = true firstRow = true
for testRunRow in testRunRowSeq: for testRunRow in allResults.data:
generateTestRunTabContentPartial(outfile, db, testRunRow, onlyFailing, firstRow) generateTestRunTabContentPartial(outfile, allResults, testRunRow, onlyFailing, firstRow)
if firstRow: if firstRow:
firstRow = false firstRow = false
outfile.generateHtmlTabContentsEnd() outfile.generateHtmlTabContentsEnd()
proc generateHtml*(filename: string, commit: int; onlyFailing: bool) = proc generateHtml*(filename: string, onlyFailing: bool) =
var db = open(connection="testament.db", user="testament", password="",
database="testament")
var outfile = open(filename, fmWrite) var outfile = open(filename, fmWrite)
outfile.generateHtmlBegin() outfile.generateHtmlBegin()
generateTestRunsHtmlPartial(outfile, db, onlyFailing) generateTestRunsHtmlPartial(outfile, allTestResults(), onlyFailing)
outfile.generateHtmlEnd() outfile.generateHtmlEnd()
outfile.flushFile() outfile.flushFile()
close(outfile) close(outfile)
close(db)
proc getCommit(db: DbConn, c: int): string =
var commit = c
for thisCommit in db.rows(sql"select id from [Commit] order by id desc"):
if commit == 0: result = thisCommit[0]
inc commit
proc generateJson*(filename: string, commit: int) =
const
selRow = """select count(*),
sum(result = 'reSuccess'),
sum(result = 'reIgnored')
from TestResult
where [commit] = ? and machine = ?
order by category"""
selDiff = """select A.category || '/' || A.target || '/' || A.name,
A.result,
B.result
from TestResult A
inner join TestResult B
on A.name = B.name and A.category = B.category
where A.[commit] = ? and B.[commit] = ? and A.machine = ?
and A.result != B.result"""
selResults = """select
category || '/' || target || '/' || name,
category, target, action, result, expected, given
from TestResult
where [commit] = ?"""
var db = open(connection="testament.db", user="testament", password="",
database="testament")
let lastCommit = db.getCommit(commit)
if lastCommit.isNil:
quit "cannot determine commit " & $commit
let previousCommit = db.getCommit(commit-1)
var outfile = open(filename, fmWrite)
let machine = $backend.getMachine(db)
let data = db.getRow(sql(selRow), lastCommit, machine)
outfile.writeLine("""{"total": $#, "passed": $#, "skipped": $#""" % data)
let results = newJArray()
for row in db.rows(sql(selResults), lastCommit):
var obj = newJObject()
obj["name"] = %row[0]
obj["category"] = %row[1]
obj["target"] = %row[2]
obj["action"] = %row[3]
obj["result"] = %row[4]
obj["expected"] = %row[5]
obj["given"] = %row[6]
results.add(obj)
outfile.writeLine(""", "results": """)
outfile.write(results.pretty)
if not previousCommit.isNil:
let diff = newJArray()
for row in db.rows(sql(selDiff), previousCommit, lastCommit, machine):
var obj = newJObject()
obj["name"] = %row[0]
obj["old"] = %row[1]
obj["new"] = %row[2]
diff.add obj
outfile.writeLine(""", "diff": """)
outfile.writeLine(diff.pretty)
outfile.writeLine "}"
close(db)
close(outfile)

View file

@ -1,7 +1,7 @@
# #
# #
# Nim Tester # Nim Tester
# (c) Copyright 2015 Andreas Rumpf # (c) Copyright 2017 Andreas Rumpf
# #
# See the file "copying.txt", included in this # See the file "copying.txt", included in this
# distribution, for details about the copyright. # distribution, for details about the copyright.
@ -12,7 +12,7 @@
import import
parseutils, strutils, pegs, os, osproc, streams, parsecfg, json, parseutils, strutils, pegs, os, osproc, streams, parsecfg, json,
marshal, backend, parseopt, specs, htmlgen, browsers, terminal, marshal, backend, parseopt, specs, htmlgen, browsers, terminal,
algorithm, compiler/nodejs, re, times, sets algorithm, compiler/nodejs, times, sets
const const
resultsFile = "testresults.html" resultsFile = "testresults.html"
@ -24,9 +24,7 @@ Command:
all run all tests all run all tests
c|category <category> run all the tests of a certain category c|category <category> run all the tests of a certain category
r|run <test> run single test file r|run <test> run single test file
html [commit] generate $1 from the database; uses the latest html generate $1 from the database
commit or a specific one (use -1 for the commit
before latest etc)
Arguments: Arguments:
arguments are passed to the compiler arguments are passed to the compiler
Options: Options:
@ -191,7 +189,12 @@ proc addResult(r: var TResults, test: TTest,
("Skipped", "") ("Skipped", "")
else: else:
("Failed", "Failure: " & $success & "\nExpected:\n" & expected & "\n\n" & "Gotten:\n" & given) ("Failed", "Failure: " & $success & "\nExpected:\n" & expected & "\n\n" & "Gotten:\n" & given)
var p = startProcess("appveyor", args=["AddTest", test.name.replace("\\", "/") & test.options, "-Framework", "nim-testament", "-FileName", test.cat.string, "-Outcome", outcome, "-ErrorMessage", msg, "-Duration", $(duration*1000).int], options={poStdErrToStdOut, poUsePath, poParentStreams}) var p = startProcess("appveyor", args=["AddTest", test.name.replace("\\", "/") & test.options,
"-Framework", "nim-testament", "-FileName",
test.cat.string,
"-Outcome", outcome, "-ErrorMessage", msg,
"-Duration", $(duration*1000).int],
options={poStdErrToStdOut, poUsePath, poParentStreams})
discard waitForExit(p) discard waitForExit(p)
close(p) close(p)
@ -290,7 +293,7 @@ proc analyzeAndConsolidateOutput(s: string): string =
result = substr(rows[i], pos) & "\n" result = substr(rows[i], pos) & "\n"
for i in i+1 ..< rows.len: for i in i+1 ..< rows.len:
result.add rows[i] & "\n" result.add rows[i] & "\n"
if not (rows[i] =~ re"^[^(]+\(\d+\)\s+"): if not (rows[i] =~ peg"['(']+ '(' \d+ ')' \s+"):
return return
elif (let pos = find(rows[i], "SIGSEGV: Illegal storage access."); pos != -1): elif (let pos = find(rows[i], "SIGSEGV: Illegal storage access."); pos != -1):
result = substr(rows[i], pos) result = substr(rows[i], pos)
@ -473,10 +476,7 @@ proc main() =
var cat = Category(subdir) var cat = Category(subdir)
processSingleTest(r, cat, p.cmdLineRest.string, file) processSingleTest(r, cat, p.cmdLineRest.string, file)
of "html": of "html":
var commit = 0 generateHtml(resultsFile, optFailing)
discard parseInt(p.cmdLineRest.string, commit)
generateHtml(resultsFile, commit, optFailing)
generateJson(jsonFile, commit)
else: else:
quit Usage quit Usage