Skip to content

Commit

Permalink
Merge pull request #3 from BinaryBirds/dev
Browse files Browse the repository at this point in the history
- md & junit output format support
  • Loading branch information
tib authored Feb 25, 2023
2 parents 8c90125 + e259a0f commit 4c1cd5e
Show file tree
Hide file tree
Showing 55 changed files with 3,027 additions and 206 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2018-2022 Tibor Bödecs
Copyright (c) 2022-2023 Binary Birds Ltd.

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
Expand Down
6 changes: 4 additions & 2 deletions makefile → Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
install:
release:
swift package update && swift build -c release
install .build/Release/testify-cli /usr/local/bin/testify

install: release
install .build/Release/testify /usr/local/bin/testify

uninstall:
rm /usr/local/bin/testify
36 changes: 29 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
// swift-tools-version:5.5
// swift-tools-version:5.7
import PackageDescription

let package = Package(
name: "Testify",
name: "testify",
platforms: [
.macOS(.v10_15),
],
products: [
.library(name: "Testify", targets: ["Testify"]),
.executable(name: "testify-cli", targets: ["testify-cli"])
.executable(name: "testify", targets: ["testify"]),
.library(name: "TestifySDK", targets: ["TestifySDK"])
],
targets: [
.executableTarget(name: "testify-cli", dependencies: ["Testify"], path: "./Sources/cli"),
.target(name: "Testify", dependencies: [], path: "./Sources/lib"),
.testTarget(name: "TestifyTests", dependencies: ["Testify"]),
.executableTarget(
name: "testify",
dependencies: [
.target(name: "TestifySDK")
]
),

// MARK: - targets

.target(
name: "TestifySDK",
dependencies: []
),

// MARK: - test targets

.testTarget(
name: "TestifySDKTests",
dependencies: [
.target(name: "TestifySDK"),
]
),
]
)
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# Testify (✅)
# Testify

Testify converts XCTest output into a proper structure (JSON), or it'll miserably fail. 😉
Testify converts XCTest output into a proper structure (JSON, JUNIT, MD), or it'll miserably fail. 😉


## Install Testify cli
## Install command line utility

You can use the cli to convert test results into JSON on the fly.

You can use the cli to convert test results into JSON on the fly.
You can use the command line utility to convert test results into JSON, JUNIT and MD on the fly.

```
git clone https://github.com/BinaryBirds/Testify.git && cd Testify
Expand All @@ -17,12 +15,18 @@ which testify

## Usage

In your projetct folder run `swift test 2>&1 | testify` or
In your project folder run:

* for JSON format: `swift test | testify json`
* for JUNIT format: `swift test | testify junit`
* for MD format: `swift test | testify md`

Just use the [Swift Package Manager](https://theswiftdev.com/2017/11/09/swift-package-manager-tutorial/) as usual:
If no argument or wrong argument added then output format fallbacks to JSON!

You can just use the [Swift Package Manager](https://theswiftdev.com/2017/11/09/swift-package-manager-tutorial/) as usual:

```swift
.package(url: "https://github.com/binarybirds/testify", from: "1.0.0"),
.package(url: "https://github.com/binarybirds/testify", from: "1.1.0"),
```

⚠️ Don't forget to add "Testify" to your target as a dependency!
Expand All @@ -32,4 +36,3 @@ Just use the [Swift Package Manager](https://theswiftdev.com/2017/11/09/swift-pa
import Testify

let suite = TestSuite.parse("test-output-string")
```
Original file line number Diff line number Diff line change
@@ -1,37 +1,81 @@
//
// TestSuite+Parse.swift
// RawTestResultParser.swift
// Testify
//
// Created by Tibor Bödecs on 2019. 01. 20..
// Created by Tibor Bodecs on 2023. 02. 12..
//

import Foundation

public extension TestSuite {
private extension String {

func match(_ pattern: String) -> String? {
let regex = try! NSRegularExpression(pattern: pattern)
let matches = regex.matches(
in: self,
range: .init(location: 0, length: count)
)
guard
let match = matches.first,
let range = Range(match.range, in: self)
else {
return nil
}
return String(self[range])
}

var matchedTestName: String? {
guard let match = match("(\\'.+\\')") else {
return nil
}
return String(match.dropFirst().dropLast())
}

static func parse(_ output: String) -> TestSuite {

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"

var matchedDate: String? {
match("(\\d{4}-\\d{2}-\\d{2}\\s\\d{2}\\:\\d{2}\\:\\d{2}\\.\\d{3})")
}

var matchedSeconds: String? {
match("(\\d+\\.\\d+)")
}

var matchedUnexpected: String? {
String(match("\\((\\d+)")!.dropFirst())
}
}

public struct RawTestResultDecoder {

private let dateFormatter: DateFormatter

public init() {
self.dateFormatter = .init()
self.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
}

public func decode(_ input: String) throws -> TestSuite {
var suites: [TestSuite] = []
var currentCaseName: String?
var testCaseOutput: String!
var gatherTestCaseOutput = false

let lines = output.split(separator: "\n").map({ String($0) })
let lines = input.split(separator: "\n").map({ String($0) })
for (index, line) in lines.enumerated() {
// start or end test suite
if line.contains("Test Suite") {
if line.contains("started") {
let name = line.matchedTestName!
let date = dateFormatter.date(from: line.matchedDate!)!

suites.append(TestSuite(name: name,
startDate: date,
endDate: date,
unexpected: 0,
outcome: .failure))
suites.append(
TestSuite(
name: name,
startDate: date,
endDate: date,
unexpected: 0,
outcome: .failure
)
)
continue;
}
else {
Expand All @@ -44,7 +88,7 @@ public extension TestSuite {
if index+1 < lines.count {
let nextLine = lines[index+1]
if nextLine.contains("Executed") {
suite.unexpected = Int(nextLine.matchedUnexpected!)!
suite.unexpected = UInt(nextLine.matchedUnexpected!)!
}
}

Expand All @@ -71,28 +115,30 @@ public extension TestSuite {
gatherTestCaseOutput = false
var suite = suites.last!
suites = Array(suites.dropLast())
let outcome: TestOutcome = line.contains("passed") ? .success : .failure
let outcome: Outcome = line.contains("passed") ? .success : .failure
let caseName = currentCaseName!.dropFirst(2).dropLast()
let firstSplit = caseName.split(separator: ".")
let secondSplit = firstSplit[1].split(separator: " ")

var failureInfo: TestFailureInfo? = nil
var failureInfo: FailureInfo? = nil
if outcome == .failure {
let outputSplit = testCaseOutput.split(separator: ":")
let file = String(outputSplit[0])
let line = Int(outputSplit[1])!
let reason = String(outputSplit.dropFirst(4)
.joined(separator: ":")
.trimmingCharacters(in: CharacterSet(charactersIn: "-").union(.whitespaces)))
failureInfo = TestFailureInfo(file: file, line: line, reason: reason)
failureInfo = FailureInfo(file: file, line: line, reason: reason)
}

let testCase = TestCase(moduleName: String(firstSplit[0]),
className: String(secondSplit[0]),
testName: String(secondSplit[1]),
duration: TimeInterval(line.matchedSeconds!)!,
outcome: outcome,
failureInfo: failureInfo)
let testCase = TestCase(
moduleName: String(firstSplit[0]),
className: String(secondSplit[0]),
testName: String(secondSplit[1]),
duration: TimeInterval(line.matchedSeconds!)!,
outcome: outcome,
failureInfo: failureInfo
)
suite.cases.append(testCase)
suites.append(suite)
currentCaseName = nil
Expand Down
24 changes: 24 additions & 0 deletions Sources/TestifySDK/Codable/TestResultCodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// TestResultCodable.swift
// Testify
//
// Created by Tibor Bodecs on 2023. 02. 12..
//

import Foundation

public enum TestResultEncoderError: Error {
case unknown
}

public enum TestResultDecoderError: Error {
case unknown
}

public protocol TestResultEncoder {
func encode(_: TestSuite) throws -> String
}

public protocol TestResultDecoder {
func decode(_: String) throws -> TestSuite
}
20 changes: 20 additions & 0 deletions Sources/TestifySDK/Codable/TestResultJSONEncoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// File.swift
//
//
// Created by Tibor Bodecs on 2023. 02. 12..
//

import Foundation

struct TestResultJSONEncoder: TestResultEncoder {

func encode(_ suite: TestSuite) throws -> String {
let encoder = JSONEncoder()
let data = try encoder.encode(suite)
guard let value = String(data: data, encoding: .utf8) else {
throw TestResultEncoderError.unknown
}
return value
}
}
68 changes: 68 additions & 0 deletions Sources/TestifySDK/Codable/TestResultJunitEncoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// File.swift
//
//
// Created by Lengyel Gábor on 2023. 02. 15..
//

import Foundation

public struct TestResultJunitEncoder: TestResultEncoder {

public init() {

}

public func encode(_ input: TestSuite) throws -> String {
var restOfResult = ""
var allTests = 0
var allTimes = 0.0
var allFails = 0

let suites: [TestSuite] = input.children.reduce([]) { $0 + $1.children }
for suite in suites {
let start = suite.startDate
let name = suite.name
let tests = suite.cases.count
let time = suite.cases.reduce(0) { $0 + $1.duration }
let failureCount = suite.cases.reduce(0) { $0 + ($1.outcome == .failure ? 1 : 0) }
allTests += tests
allTimes += time
allFails += failureCount
restOfResult += "<testsuite id=\"\(name)\" name=\"\(name)\" tests=\"\(tests)\" skipped=\"0\" failures=\"\(failureCount)\" errors=\"0\" timestamp=\"\(start)\" hostname=\"JunitEncoder\" time=\"\(time)\">\n"

for testCase in suite.cases {
let name = testCase.testName
let className = testCase.className
let success = testCase.outcome == .success
let time = testCase.duration
let failLine = testCase.failureInfo?.line
let failReason = testCase.failureInfo?.reason

restOfResult += "<testcase name=\"\(name)\" classname=\"\(className)\" time=\"\(time)\""
if (success) {
restOfResult += "/>\n"
} else {
if (testCase.failureInfo != nil) {
restOfResult += ">\n"
restOfResult += "<failure message=\"\(failLine ?? -1)\" type=\"type\">\n"
restOfResult += "\(failReason ?? "")\n"
restOfResult += "</failure>\n"
restOfResult += "</testcase>\n"
} else {
restOfResult += "/>\n"
}
}
}
restOfResult += "</testsuite>\n"
}
restOfResult += "</testsuites>\n"

let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd_HHmmss"
var startResult = "\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
startResult += "<testsuites id=\"\(formatter.string(from: input.startDate))\" name=\"\(input.name)\" tests=\"\(allTests)\" failures=\"\(allFails)\" time=\"\(allTimes)\">\n"

return startResult + restOfResult
}
}
Loading

0 comments on commit 4c1cd5e

Please sign in to comment.