Skip to content

Commit

Permalink
Added a readable HTML test report to Bluepill (#387)
Browse files Browse the repository at this point in the history
* Added a readable HTML test report to Bluepill

BPHTMLReportWrite takes the NSXMLDocument object which is the JUnit report
  and generate an HTML page with a JS file.
  • Loading branch information
chenxiao0228 authored and ob committed Nov 13, 2019
1 parent e763203 commit 58bc0e3
Show file tree
Hide file tree
Showing 15 changed files with 693 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Bluepill Specific file
bp/BPVersion.h
bluepill/src/BPTestReportHTML.h
result.txt

# Xcode
Expand Down
12 changes: 12 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,15 @@ genrule(
tools = ["scripts/bpversion.sh"],
visibility = ["//visibility:public"],
)

genrule(
name = "html_header",
srcs = ["scripts/0-test-report.html"],
outs = ["BPTestReportHTML.h"],
cmd = "./$(location scripts/html_header.sh) \"$@\"",
local = True,
stamp = 1,
tags = ["local", "no-cache"],
tools = ["scripts/html_header.sh"],
visibility = ["//visibility:public"],
)
2 changes: 1 addition & 1 deletion bluepill/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ objc_library(
srcs = glob([
"src/**/*.h",
"src/**/*.m",
]),
]) + ["//:html_header"],
deps = ["//bp:bpsrc"],
)

Expand Down
62 changes: 62 additions & 0 deletions bluepill/bluepill.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
objects = {

/* Begin PBXBuildFile section */
015A70B72367A8690073484F /* BPHTMLReportWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = 0173520B2366110D008BFA4E /* BPHTMLReportWriter.m */; };
0173520C2366110D008BFA4E /* BPHTMLReportWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = 0173520B2366110D008BFA4E /* BPHTMLReportWriter.m */; };
0173520F23679E0A008BFA4E /* BPHTMLReportWriteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0173520E23679E0A008BFA4E /* BPHTMLReportWriteTests.m */; };
0173521323679E87008BFA4E /* TEST-FinalReport.xml in Resources */ = {isa = PBXBuildFile; fileRef = 0173521223679E87008BFA4E /* TEST-FinalReport.xml */; };
56B74BCA1E4C0A15004E6624 /* BPIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 56B74BC91E4C0A15004E6624 /* BPIntegrationTests.m */; };
B3109F792151F72F00B9309C /* CoreSimulator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3380AEE2150BD8700752E1B /* CoreSimulator.framework */; };
BA1809E91DBA8FC300D7D130 /* BPRunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BA1809E81DBA8FC300D7D130 /* BPRunnerTests.m */; };
Expand Down Expand Up @@ -34,7 +38,22 @@
E49235FF22EA847700395D98 /* times.json in Resources */ = {isa = PBXBuildFile; fileRef = E49235FE22EA847700395D98 /* times.json */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
0137FEA1237B879700B36E69 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = BAEF4B2C1DAC539400E68294 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BAEF4B331DAC539400E68294;
remoteInfo = bluepill;
};
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
0173520A2366110D008BFA4E /* BPHTMLReportWriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BPHTMLReportWriter.h; sourceTree = "<group>"; };
0173520B2366110D008BFA4E /* BPHTMLReportWriter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BPHTMLReportWriter.m; sourceTree = "<group>"; };
0173520D2366186A008BFA4E /* BPTestReportHTML.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BPTestReportHTML.h; sourceTree = "<group>"; };
0173520E23679E0A008BFA4E /* BPHTMLReportWriteTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BPHTMLReportWriteTests.m; sourceTree = "<group>"; };
0173521223679E87008BFA4E /* TEST-FinalReport.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "TEST-FinalReport.xml"; sourceTree = "<group>"; };
56B74BC91E4C0A15004E6624 /* BPIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BPIntegrationTests.m; sourceTree = "<group>"; };
B3380AEE2150BD8700752E1B /* CoreSimulator.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreSimulator.framework; path = ../../../../../../../Library/Developer/PrivateFrameworks/CoreSimulator.framework; sourceTree = "<group>"; };
BA1809E01DBA8FB100D7D130 /* bluepill-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "bluepill-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -108,6 +127,7 @@
BA23EF601EF8ACF10074A4EF /* BPPackerTests.m */,
BAD8484C1DBC6BA2007034CF /* BPReportCollectorTests.m */,
BA1809E81DBA8FC300D7D130 /* BPRunnerTests.m */,
0173520E23679E0A008BFA4E /* BPHTMLReportWriteTests.m */,
BA1809E41DBA8FB100D7D130 /* Info.plist */,
);
path = tests;
Expand Down Expand Up @@ -140,6 +160,7 @@
BA9C2DAD1DD674AD007CB967 /* Resource Files */ = {
isa = PBXGroup;
children = (
0173521223679E87008BFA4E /* TEST-FinalReport.xml */,
E49235FE22EA847700395D98 /* times.json */,
BA9C2DB01DD67B66007CB967 /* testScheme.xcscheme */,
BA9C2DAE1DD674AD007CB967 /* BPSampleAppTests.xctest */,
Expand Down Expand Up @@ -187,6 +208,9 @@
C4FD8C571DB6E09B000ED28C /* BPPacker.m */,
BAD848481DBC6A83007034CF /* BPReportCollector.h */,
BAD848491DBC6A83007034CF /* BPReportCollector.m */,
0173520A2366110D008BFA4E /* BPHTMLReportWriter.h */,
0173520B2366110D008BFA4E /* BPHTMLReportWriter.m */,
0173520D2366186A008BFA4E /* BPTestReportHTML.h */,
C41C41F71DB14B5F001F32A2 /* BPRunner.h */,
C41C41F81DB14B5F001F32A2 /* BPRunner.m */,
BAEF4B371DAC539400E68294 /* main.m */,
Expand Down Expand Up @@ -216,6 +240,7 @@
buildRules = (
);
dependencies = (
0137FEA2237B879700B36E69 /* PBXTargetDependency */,
);
name = "bluepill-tests";
productName = BluepillRunnerTests;
Expand All @@ -226,6 +251,7 @@
isa = PBXNativeTarget;
buildConfigurationList = BAEF4B3B1DAC539400E68294 /* Build configuration list for PBXNativeTarget "bluepill" */;
buildPhases = (
0137FE97237B65E100B36E69 /* Generate HTML template header */,
BAEF4B301DAC539400E68294 /* Sources */,
BAEF4B311DAC539400E68294 /* Frameworks */,
);
Expand Down Expand Up @@ -289,12 +315,37 @@
BA7031881DE4ED2F008B3539 /* result1.xml in Resources */,
BA9C2DAF1DD674AD007CB967 /* BPSampleAppTests.xctest in Resources */,
E49235FF22EA847700395D98 /* times.json in Resources */,
0173521323679E87008BFA4E /* TEST-FinalReport.xml in Resources */,
BA7031891DE4ED2F008B3539 /* result2.xml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
0137FE97237B65E100B36E69 /* Generate HTML template header */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"\"${SOURCE_ROOT}/../scripts/0-test-report.html\"",
"\"${SOURCE_ROOT}/../scripts/html_header.sh\"",
);
name = "Generate HTML template header";
outputFileListPaths = (
);
outputPaths = (
"\"${SOURCE_ROOT}/src/BPTestReportHTML.h\"",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "${SOURCE_ROOT}/../scripts/html_header.sh \"${SOURCE_ROOT}/src/BPTestReportHTML.h\"\n";
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
BA1809DC1DBA8FB100D7D130 /* Sources */ = {
isa = PBXSourcesBuildPhase;
Expand All @@ -303,13 +354,15 @@
BA1809E91DBA8FC300D7D130 /* BPRunnerTests.m in Sources */,
BA1809FB1DBA949600D7D130 /* BPPacker.m in Sources */,
BA1809EB1DBA910400D7D130 /* BPAppTests.m in Sources */,
015A70B72367A8690073484F /* BPHTMLReportWriter.m in Sources */,
BA1809FA1DBA949600D7D130 /* BPRunner.m in Sources */,
C467E54C1DC94BB200BC80EE /* BPCLITests.m in Sources */,
BAD8484D1DBC6BA2007034CF /* BPReportCollectorTests.m in Sources */,
BA1809FD1DBA949600D7D130 /* BPApp.m in Sources */,
BAD8484B1DBC6A86007034CF /* BPReportCollector.m in Sources */,
BA23EF611EF8ACF10074A4EF /* BPPackerTests.m in Sources */,
56B74BCA1E4C0A15004E6624 /* BPIntegrationTests.m in Sources */,
0173520F23679E0A008BFA4E /* BPHTMLReportWriteTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -320,13 +373,22 @@
C4FD8C581DB6E09B000ED28C /* BPPacker.m in Sources */,
BAEF4B381DAC539400E68294 /* main.m in Sources */,
C41C41F91DB14B5F001F32A2 /* BPRunner.m in Sources */,
0173520C2366110D008BFA4E /* BPHTMLReportWriter.m in Sources */,
BAD8484A1DBC6A83007034CF /* BPReportCollector.m in Sources */,
C41C41F31DB04032001F32A2 /* BPApp.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */

/* Begin PBXTargetDependency section */
0137FEA2237B879700B36E69 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BAEF4B331DAC539400E68294 /* bluepill */;
targetProxy = 0137FEA1237B879700B36E69 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */

/* Begin XCBuildConfiguration section */
BA1809E51DBA8FB100D7D130 /* Debug */ = {
isa = XCBuildConfiguration;
Expand Down
26 changes: 26 additions & 0 deletions bluepill/src/BPHTMLReportWriter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2016 LinkedIn Corporation
// Licensed under the BSD 2-Clause License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at https://opensource.org/licenses/BSD-2-Clause
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

// The class to write a more readable HTML report from JUnit report.
@interface BPHTMLReportWriter : NSObject

/*!
* @discussion write a readable HRML report
* @param jUnitReport the junit report
* @param folderPath the output folder path
*/
- (void)writeHTMLReportWithJUnitReport:(NSXMLDocument *)jUnitReport inFolder:(NSString *)folderPath;

@end

NS_ASSUME_NONNULL_END
160 changes: 160 additions & 0 deletions bluepill/src/BPHTMLReportWriter.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2016 LinkedIn Corporation
// Licensed under the BSD 2-Clause License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at https://opensource.org/licenses/BSD-2-Clause
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

#import <Foundation/Foundation.h>
#import "BPTestReportHTML.h"
#import "bp/src/BPUtils.h"

#import "BPHTMLReportWriter.h"

static const NSString * const kHTMLPath = @"0-test-report.html";
static const NSString * const kJSPath = @"test-report.js";
static const NSString * const kFailureLogsPath = @"failure-logs";

@implementation BPHTMLReportWriter

- (NSDictionary<NSString*, NSString*> *)metaForFileWrites:(NSXMLDocument *)jUnitReport
inFolder:(nonnull NSString *)folderPath {
NSMutableDictionary *metaData = [[NSMutableDictionary alloc] init];
NSString *htmlPath = [folderPath stringByAppendingPathComponent:[kHTMLPath copy]];
NSString *jsPath = [folderPath stringByAppendingPathComponent:[kJSPath copy]];
NSString *failureLogsPath = [folderPath stringByAppendingPathComponent:[kFailureLogsPath copy]];
// HTML data
metaData[htmlPath] = kHTMLContent;
[BPUtils printInfo:INFO withString:@"HTML report: %@", htmlPath];

// write the JS goes with the html
if (jUnitReport.childCount == 0) {
[BPUtils printInfo:WARNING withString:@"Bluepill JUnit report has no data."];
return nil;
}
NSMutableArray<NSString *> *jsLines = [[NSMutableArray alloc] initWithArray:@[
@"var json = {",
@"\t\"product\": \"All Tests\",\n",
@"\t\"testSuites\": [",
@"\t\t{",
@"\t\t\t\"testCases\": ["
]];
// testsuites
// <testsuites name="AllTestUnits" tests="17" failures="0" errors="1" time="53.990912">
NSXMLNode *rootNode = jUnitReport.children[0];
for (NSXMLNode *testSuiteNode in rootNode.children) {
if (testSuiteNode.kind == NSXMLElementKind && [testSuiteNode.name isEqualToString:@"testsuite"]) {
// testsuite
// <testsuite tests="17" failures="0" errors="1" time="53.990912" timestamp="2016-02-25T10:52:05GMT-08:00" name="Toplevel Test Suite">
for (NSXMLNode *testClassNode in testSuiteNode.children) {
for (NSXMLNode *testCaseNode in testClassNode.children) {
if (testCaseNode.kind == NSXMLElementKind && [testCaseNode.name isEqualToString:@"testcase"]) {
// testcase
// product-dashboard <testcase classname="FeedChannelUpdateTest" name="testChannelUpdateInMiniFeed" time="20.197627">
BOOL rc = YES;
NSXMLElement *testCaseElement = (NSXMLElement *)testCaseNode;
NSMutableArray<NSDictionary *> *errors = [[NSMutableArray alloc] init];
NSMutableArray<NSString *> *logs = [[NSMutableArray alloc] init];
for (NSXMLNode *node in testCaseNode.children) {
if (node.kind != NSXMLElementKind) {
[BPUtils printInfo:WARNING withString:@"Invalid node type: %@ is not an XMLElement", node.name];
}
NSXMLElement *element = (NSXMLElement *)node;
if ([node.name isEqualToString:@"failure"] || [node.name isEqualToString:@"error"]) {
rc = NO;
NSDictionary *error = @{
@"message": [[element attributeForName:@"message"] stringValue],
@"location": element.children.firstObject.stringValue
};
[errors addObject:error];
} else if ([node.name isEqualToString:@"system-out"]) {
// report system-out for failures only
[logs addObject:node.children.firstObject.stringValue];
}
}
NSString *className = [[testCaseElement attributeForName:@"classname"] stringValue];
NSString *caseName = [[testCaseElement attributeForName:@"name"] stringValue];
[jsLines addObject:@"\t\t\t\t{"];
[jsLines addObject:[NSString stringWithFormat:@"\t\t\t\t\t\"className\": \"%@\",", className]];
[jsLines addObject:[NSString stringWithFormat:@"\t\t\t\t\t\"name\": \"%@\",", caseName]];
if (!rc) {
[jsLines addObject:@"\t\t\t\t\t\"failed\": true,"];
NSMutableArray *errorMessages = [[NSMutableArray alloc] init];
for (NSDictionary *errorDict in errors) {
NSString *errorMessage = [NSString stringWithFormat:@"\"message\": \"%@\", \"location\": \"%@\"",
[self escapeStringForJS:errorDict[@"message"]],
[self escapeStringForJS:errorDict[@"location"]]];
[errorMessages addObject:errorMessage];
}
[jsLines addObject:[NSString stringWithFormat:@"\t\t\t\t\t\"errors\": [{%@}],",
[errorMessages componentsJoinedByString:@"},{"]]];
NSMutableArray<NSString *> *artifacts = [[NSMutableArray alloc] init];
for (NSString *log in logs) {
NSString *logName = [NSString stringWithFormat:@"%@.%@.txt", className, caseName];
NSString *logPath = [failureLogsPath stringByAppendingPathComponent:logName];
metaData[logPath] = log;
NSString *relativePath = [NSString stringWithFormat:@"%@/%@", kFailureLogsPath, logName];
[artifacts addObject:relativePath];
}
if ([artifacts count] > 0) {
[jsLines addObject:[NSString stringWithFormat:@"\t\t\t\t\t\"artifacts\": [\"%@\"],\n",
[artifacts componentsJoinedByString:@"\",\""]]];
}
}
NSString *caseTime = [[testCaseElement attributeForName:@"time"] stringValue];
[jsLines addObject:[NSString stringWithFormat:@"\t\t\t\t\t\"time\": %.3f", [caseTime floatValue]]];
[jsLines addObject:@"\t\t\t\t},"];
}
}
}
}
}
if(rootNode.kind != NSXMLElementKind) {
[BPUtils printInfo:WARNING withString:@"Bluepill JUnit report has no data."];
}
NSXMLElement *rootElement = (NSXMLElement *)rootNode;
NSString *numFailures = [[rootElement attributeForName:@"failures"] stringValue];
NSString *numTests = [[rootElement attributeForName:@"tests"] stringValue];
float time = [[[rootElement attributeForName:@"time"] stringValue] floatValue];
[jsLines addObjectsFromArray:@[
@"\t\t\t],",
@"\t\t\t\"name\": \"All Tests\",",
[NSString stringWithFormat:@"\t\t\t\"numFailures\": %@,", numFailures],
[NSString stringWithFormat:@"\t\t\t\"numTests\": %@,", numTests],
[NSString stringWithFormat:@"\t\t\t\"time\": %.3f,", time],
@"\t\t},",
@"\t],"
]];
if ([numFailures integerValue] > 0) {
[jsLines addObject:@"\t\"failed\": true,"];
}
[jsLines addObject:@"}\n"];
metaData[jsPath] = [jsLines componentsJoinedByString:@"\n"];
return metaData;
}


- (void)writeHTMLReportWithJUnitReport:(NSXMLDocument *)jUnitReport
inFolder:(nonnull NSString *)folderPath {
NSDictionary<NSString*, NSString*> *metaData = [self metaForFileWrites:jUnitReport inFolder:folderPath];
NSString *failureLogsPath = [folderPath stringByAppendingPathComponent:[kFailureLogsPath copy]];
// create failure logs folder
[[NSFileManager defaultManager] createDirectoryAtPath:failureLogsPath
withIntermediateDirectories:YES
attributes:nil
error:nil];
for (NSString *key in metaData.allKeys) {
[[metaData[key] dataUsingEncoding:NSUTF8StringEncoding] writeToFile:key atomically:YES];
}
}

- (NSString *)escapeStringForJS:(NSString *)jsCode {
NSString *ret = [jsCode stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
ret = [ret stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
ret = [ret stringByReplacingOccurrencesOfString:@"\r" withString:@""];
return ret;
}

@end
Loading

0 comments on commit 58bc0e3

Please sign in to comment.