Skip to content

Commit

Permalink
support multiple sources (via #70)
Browse files Browse the repository at this point in the history
  • Loading branch information
eroshenkoam authored Apr 10, 2024
1 parent e727075 commit f19db33
Show file tree
Hide file tree
Showing 4 changed files with 344 additions and 274 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ Below are a few examples of common commands. For further assistance, use the --h

### Export to Allure2 results

`xcresults export /path/to/Test.xcresult /path/to/outputDirectory`
Single source:
`xcresults export /path/to/Test.xcresult -o /path/to/outputDirectory`

Multiple sources:
`xcresults export /path/to/First.xcresult /path/to/Second.xcresult -o /path/to/outputDirectory`

After that you can generate Allure report by following command:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static io.eroshenkoam.xcresults.export.ExportCommand.FILE_EXTENSION_HEIC;
import static io.eroshenkoam.xcresults.export.ExportProcessor.FILE_EXTENSION_HEIC;
import static io.eroshenkoam.xcresults.util.FormatUtil.getAttachmentFileName;
import static io.eroshenkoam.xcresults.util.FormatUtil.parseDate;
import static java.util.Objects.isNull;
Expand Down
306 changes: 34 additions & 272 deletions src/main/java/io/eroshenkoam/xcresults/export/ExportCommand.java
Original file line number Diff line number Diff line change
@@ -1,70 +1,20 @@
package io.eroshenkoam.xcresults.export;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.eroshenkoam.xcresults.carousel.CarouselPostProcessor;
import io.qameta.allure.model.ExecutableItem;
import io.qameta.allure.model.TestResult;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import picocli.CommandLine;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

import static io.eroshenkoam.xcresults.util.FormatUtil.getResultFilePath;
import static io.eroshenkoam.xcresults.util.FormatUtil.parseDate;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

@CommandLine.Command(
name = "export", mixinStandardHelpOptions = true,
description = "Export XC test results to json with attachments"
)
public class ExportCommand implements Runnable {

public static final String FILE_EXTENSION_HEIC = "heic";

private static final String ACTIONS = "actions";
private static final String ACTION_RESULT = "actionResult";

private static final String RUN_DESTINATION = "runDestination";
private static final String START_TIME = "startedTime";

private static final String SUMMARIES = "summaries";
private static final String TESTABLE_SUMMARIES = "testableSummaries";

private static final String TESTS = "tests";
private static final String SUBTESTS = "subtests";

private static final String FAILURE_SUMMARIES = "failureSummaries";
private static final String ACTIVITY_SUMMARIES = "activitySummaries";
private static final String SUBACTIVITIES = "subactivities";

private static final String ATTACHMENTS = "attachments";

private static final String FILENAME = "filename";
private static final String PAYLOAD_REF = "payloadRef";

private static final String SUMMARY_REF = "summaryRef";

private static final String SUITE = "suite";

private static final String ID = "id";
private static final String TYPE = "_type";
private static final String NAME = "_name";
private static final String VALUE = "_value";
private static final String VALUES = "_values";
private static final String DISPLAY_NAME = "displayName";
private static final String TARGET_NAME = "targetName";

private static final String TEST_REF = "testsRef";

private final ObjectMapper mapper = new ObjectMapper()
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);

@CommandLine.Option(
names = {"--format"},
description = "Export format (json, allure2), *deprecated"
Expand All @@ -84,248 +34,60 @@ public class ExportCommand implements Runnable {
private String carouselTemplatePath;

@CommandLine.Parameters(
index = "0",
description = "The directories with *.xcresults"
)
protected Path inputPath;
protected List<Path> inputPath;

@CommandLine.Parameters(
index = "1",
@CommandLine.Option(
names = {"-o", "--output"},
description = "Export output directory"
)
protected Path outputPath;

@Override
public void run() {
try {
runUnsafe();
} catch (Exception e) {
e.printStackTrace(System.out);
}
}

private void runUnsafe() throws Exception {
final JsonNode node = readSummary();
if (Files.exists(outputPath)) {
System.out.println("Delete existing output directory...");
FileUtils.deleteDirectory(outputPath.toFile());
}
Files.createDirectories(outputPath);

final Map<String, ExportMeta> testRefIds = new HashMap<>();
for (JsonNode action : node.get(ACTIONS).get(VALUES)) {
if (action.get(ACTION_RESULT).has(TEST_REF)) {
final ExportMeta meta = new ExportMeta();
if (action.has(RUN_DESTINATION)) {
meta.label(RUN_DESTINATION, action.get(RUN_DESTINATION).get(DISPLAY_NAME).get(VALUE).asText());
}
if (action.has(START_TIME)) {
meta.setStart(parseDate(action.get(START_TIME).get(VALUE).textValue()));
}
testRefIds.put(action.get(ACTION_RESULT).get(TEST_REF).get(ID).get(VALUE).asText(), meta);
final List<Path> input = getInputPaths();
final Path output = getOutputPath();
if (Objects.isNull(output)) {
System.out.println("Output path [-o, --output] is required");
return;
}
}

final Map<JsonNode, ExportMeta> testSummaries = new HashMap<>();
testRefIds.forEach((testRefId, meta) -> {
final JsonNode testRef = getReference(testRefId);
for (JsonNode summary : testRef.get(SUMMARIES).get(VALUES)) {
for (JsonNode testableSummary : summary.get(TESTABLE_SUMMARIES).get(VALUES)) {
final ExportMeta testMeta = getTestMeta(meta, testableSummary);
if (testableSummary.has(TESTS) && testableSummary.get(TESTS).has(VALUES)) {
for (JsonNode test : testableSummary.get(TESTS).get(VALUES)) {
getTestSummaries(test).forEach(testSummary -> {
testSummaries.put(testSummary, testMeta);
});
}
} else {
System.out.printf("No tests found for '%s'%n", testableSummary.get("name").get(VALUE));
}
}
if (Files.exists(output)) {
System.out.println("Delete existing output directory...");
FileUtils.deleteDirectory(output.toFile());
}
});

System.out.printf("Export information about %s test summaries...%n", testSummaries.size());
final Map<String, String> attachmentsRefs = new HashMap<>();
final Map<Path, TestResult> testResults = new HashMap<>();
for (final Map.Entry<JsonNode, ExportMeta> entry : testSummaries.entrySet()) {
final JsonNode testSummary = entry.getKey();
final ExportMeta meta = entry.getValue();

final TestResult testResult = new Allure2ExportFormatter().format(meta, testSummary);
final Path testSummaryPath = getResultFilePath(outputPath);
mapper.writeValue(testSummaryPath.toFile(), testResult);

final Map<String, List<String>> attachmentSources = getAttachmentSources(testResult);
final List<JsonNode> summaries = new ArrayList<>();
summaries.addAll(getAttributeValues(testSummary, ACTIVITY_SUMMARIES));
summaries.addAll(getAttributeValues(testSummary, FAILURE_SUMMARIES));
summaries.forEach(summary -> {
getAttachmentRefs(summary).forEach((name, ref) -> {
if (attachmentSources.containsKey(name)) {
final List<String> sources = attachmentSources.get(name);
sources.forEach(source -> attachmentsRefs.put(source, ref));
}
});
});
testResults.put(testSummaryPath, testResult);
}
System.out.printf("Export information about %s attachments...%n", attachmentsRefs.size());
for (Map.Entry<String, String> attachment : attachmentsRefs.entrySet()) {
final String attachmentRef = attachment.getValue();
final Path attachmentPath = outputPath.resolve(attachment.getKey());
exportReference(attachmentRef, attachmentPath);
}
final List<ExportPostProcessor> postProcessors = new ArrayList<>();
if (Objects.nonNull(addCarouselAttachment)) {
postProcessors.add(new CarouselPostProcessor(carouselTemplatePath));
}
postProcessors.forEach(postProcessor -> postProcessor.processTestResults(outputPath, testResults));
}

private ExportMeta getTestMeta(final ExportMeta meta, final JsonNode testableSummary) {
final ExportMeta exportMeta = new ExportMeta();
exportMeta.setStart(meta.getStart());
meta.getLabels().forEach(exportMeta::label);
exportMeta.label(SUITE, testableSummary.get(TARGET_NAME).get(VALUE).asText());
return exportMeta;
}

private Map<String, List<String>> getAttachmentSources(final ExecutableItem executableItem) {
final Map<String, List<String>> attachments = new HashMap<>();
if (Objects.nonNull(executableItem.getAttachments())) {
executableItem.getAttachments().forEach(a -> {
final List<String> sources = attachments.getOrDefault(a.getName(), new ArrayList<>());
sources.add(a.getSource());
attachments.put(a.getName(), sources);
});
}
if (Objects.nonNull(executableItem.getSteps())) {
executableItem.getSteps().forEach(s -> attachments.putAll(getAttachmentSources(s)));
}
return attachments;
}

private Map<String, String> getAttachmentRefs(final JsonNode test) {
final Map<String, String> refs = new HashMap<>();
if (test.has(ATTACHMENTS)) {
for (final JsonNode attachment : test.get(ATTACHMENTS).get(VALUES)) {
if (attachment.has(PAYLOAD_REF)) {
final String fileName = attachment.get(FILENAME).get(VALUE).asText();
final String attachmentRef = attachment.get(PAYLOAD_REF).get(ID).get(VALUE).asText();
refs.put(fileName, attachmentRef);
}
}
}
if (test.has(SUBACTIVITIES)) {
for (final JsonNode subActivity : test.get(SUBACTIVITIES).get(VALUES)) {
refs.putAll(getAttachmentRefs(subActivity));
}
}
return refs;
}

private List<JsonNode> getTestSummaries(final JsonNode test) {
final List<JsonNode> summaries = new ArrayList<>();
if (test.has(SUMMARY_REF)) {
final String ref = test.get(SUMMARY_REF).get(ID).get(VALUE).asText();
summaries.add(getReference(ref));
} else {
if (test.has(TYPE) && test.get(TYPE).get(NAME).textValue().equals("ActionTestMetadata")) {
summaries.add(test);
}
}

if (test.has(SUBTESTS)) {
for (final JsonNode subTest : test.get(SUBTESTS).get(VALUES)) {
summaries.addAll(getTestSummaries(subTest));
Files.createDirectories(output);
for (Path path: input) {
runUnsafe(path, output);
}
} catch (Exception e) {
e.printStackTrace(System.out);
}
return summaries;
}

private List<JsonNode> getAttributeValues(final JsonNode node, final String attributeName) {
final List<JsonNode> result = new ArrayList<>();
if (node.has(attributeName) && node.get(attributeName).has(VALUES)) {
node.get(attributeName).get(VALUES).forEach(result::add);
}
return result;
}

private JsonNode readSummary() {
final ProcessBuilder builder = new ProcessBuilder();
builder.command(
"xcrun",
"xcresulttool",
"get",
"--format", "json",
"--path", inputPath.toAbsolutePath().toString()
);
return readProcessOutput(builder);
}

private JsonNode getReference(final String id) {
final ProcessBuilder builder = new ProcessBuilder();
builder.command(
"xcrun",
"xcresulttool",
"get",
"--format", "json",
"--path", inputPath.toAbsolutePath().toString(),
"--id", id
private void runUnsafe(final Path input, final Path output) throws Exception {
System.out.printf("Export xcresults from [%s] to [%s]\n", input, output);
final ExportProcessor processor = new ExportProcessor(
input, output, addCarouselAttachment, carouselTemplatePath
);
return readProcessOutput(builder);
processor.export();
}

private void exportReference(final String id, final Path output) {
final ProcessBuilder exportBuilder = new ProcessBuilder();
exportBuilder.command(
"xcrun",
"xcresulttool",
"export",
"--type", "file",
"--path", inputPath.toAbsolutePath().toString(),
"--id", id,
"--output-path", output.toAbsolutePath().toString()
);
readProcessOutput(exportBuilder);
if (FILE_EXTENSION_HEIC.equals(FilenameUtils.getExtension(output.toString()))) {
convertHeicToJpeg(output);
private List<Path> getInputPaths() {
if (inputPath.size() == 2 && Objects.isNull(outputPath)) {
return Arrays.asList(inputPath.get(0));
} else {
return inputPath;
}
}

private void convertHeicToJpeg(Path heicPath) {
try {
final Path parent = heicPath.getParent();
final String jpegFilename = String.format("%s.%s", FilenameUtils.getBaseName(heicPath.toString()), "jpeg");
final Path jpegFilePath = parent.resolve(jpegFilename);
final ProcessBuilder convertBuilder = new ProcessBuilder();
convertBuilder.command(
"sips", "-s",
"format", "jpeg",
heicPath.toAbsolutePath().toString(),
"--out", jpegFilePath.toAbsolutePath().toString()
);
Process process = convertBuilder.start();
process.waitFor();
FileUtils.deleteQuietly(heicPath.toFile());
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
private Path getOutputPath() {
if (inputPath.size() == 2 && Objects.isNull(outputPath)) {
return inputPath.get(1);
} else {
return outputPath;
}
}

private JsonNode readProcessOutput(final ProcessBuilder builder) {
try {
final Process process = builder.start();
try (InputStream input = process.getInputStream()) {
if (Objects.nonNull(input)) {
return mapper.readTree(input);
} else {
return null;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Loading

0 comments on commit f19db33

Please sign in to comment.