Skip to content

Commit 2354fb1

Browse files
authored
make Allure result files durable before publishing (via #1294)
1 parent ea44303 commit 2354fb1

2 files changed

Lines changed: 141 additions & 15 deletions

File tree

allure-java-commons/src/main/java/io/qameta/allure/FileSystemResultsWriter.java

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@
2222
import org.slf4j.Logger;
2323
import org.slf4j.LoggerFactory;
2424

25+
import java.io.DataOutput;
26+
import java.io.DataOutputStream;
2527
import java.io.IOException;
2628
import java.io.InputStream;
29+
import java.io.OutputStream;
30+
import java.nio.channels.Channels;
31+
import java.nio.channels.FileChannel;
2732
import java.nio.file.Files;
2833
import java.nio.file.Path;
34+
import java.nio.file.StandardCopyOption;
35+
import java.nio.file.StandardOpenOption;
2936
import java.util.Comparator;
3037
import java.util.Objects;
3138
import java.util.UUID;
@@ -41,6 +48,12 @@ public class FileSystemResultsWriter implements AllureResultsWriter {
4148

4249
private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemResultsWriter.class);
4350

51+
private static final String TEST_RESULT_ENTITY_NAME = "test result";
52+
53+
private static final String TEST_RESULT_CONTAINER_ENTITY_NAME = "test result container";
54+
55+
private static final String ATTACHMENT_ENTITY_NAME = "attachment";
56+
4457
private final Path outputDirectory;
4558

4659
private final ObjectMapper mapper;
@@ -84,13 +97,11 @@ public void write(final TestResult testResult) {
8497
final String testResultName = Objects.isNull(testResult.getUuid())
8598
? generateTestResultName()
8699
: generateTestResultName(testResult.getUuid());
87-
ensureInitialized();
88100
final Path file = outputDirectory.resolve(testResultName);
89-
try {
90-
mapper.writeValue(file.toFile(), testResult);
91-
} catch (IOException e) {
92-
throw new AllureResultsWriteException("Could not write Allure test result", e);
93-
}
101+
write(file, TEST_RESULT_ENTITY_NAME, channel -> {
102+
final DataOutput output = new DataOutputStream(Channels.newOutputStream(channel));
103+
mapper.writeValue(output, testResult);
104+
});
94105
}
95106

96107
/**
@@ -101,26 +112,63 @@ public void write(final TestResultContainer testResultContainer) {
101112
final String testResultContainerName = Objects.isNull(testResultContainer.getUuid())
102113
? generateTestResultContainerName()
103114
: generateTestResultContainerName(testResultContainer.getUuid());
104-
ensureInitialized();
105115
final Path file = outputDirectory.resolve(testResultContainerName);
106-
try {
107-
mapper.writeValue(file.toFile(), testResultContainer);
108-
} catch (IOException e) {
109-
throw new AllureResultsWriteException("Could not write Allure test result container", e);
110-
}
116+
write(file, TEST_RESULT_CONTAINER_ENTITY_NAME, channel -> {
117+
final DataOutput output = new DataOutputStream(Channels.newOutputStream(channel));
118+
mapper.writeValue(output, testResultContainer);
119+
});
111120
}
112121

113122
/**
114123
* {@inheritDoc}
115124
*/
116125
@Override
117126
public void write(final String source, final InputStream attachment) {
118-
ensureInitialized();
119127
final Path file = outputDirectory.resolve(source);
120128
try (InputStream is = attachment) {
121-
Files.copy(is, file);
129+
write(file, ATTACHMENT_ENTITY_NAME, channel -> {
130+
final OutputStream output = Channels.newOutputStream(channel);
131+
is.transferTo(output);
132+
});
133+
} catch (IOException e) {
134+
throw new AllureResultsWriteException(getErrorMessage(ATTACHMENT_ENTITY_NAME), e);
135+
}
136+
}
137+
138+
private void write(final Path file, final String entityName, final ChannelConsumer consumer) {
139+
ensureInitialized();
140+
Path tempFile = null;
141+
try {
142+
tempFile = Files.createTempFile(outputDirectory, ".allure-write-", ".tmp");
143+
try (FileChannel channel = FileChannel.open(tempFile, StandardOpenOption.WRITE)) {
144+
consumer.accept(channel);
145+
channel.force(true);
146+
}
147+
Files.move(tempFile, file, StandardCopyOption.REPLACE_EXISTING);
148+
tempFile = null;
149+
} catch (IOException e) {
150+
deleteIfExists(tempFile);
151+
throw new AllureResultsWriteException(getErrorMessage(entityName), e);
152+
}
153+
}
154+
155+
private static String getErrorMessage(final String entityName) {
156+
return "Could not write Allure " + entityName;
157+
}
158+
159+
private interface ChannelConsumer {
160+
161+
void accept(FileChannel channel) throws IOException;
162+
}
163+
164+
private static void deleteIfExists(final Path file) {
165+
if (Objects.isNull(file)) {
166+
return;
167+
}
168+
try {
169+
Files.deleteIfExists(file);
122170
} catch (IOException e) {
123-
throw new AllureResultsWriteException("Could not write Allure attachment", e);
171+
LOGGER.warn("Failed to delete temporary Allure result file {}", file, e);
124172
}
125173
}
126174

allure-java-commons/src/test/java/io/qameta/allure/FileSystemResultsWriterTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,28 @@
1717

1818
import io.qameta.allure.model.StatusDetails;
1919
import io.qameta.allure.model.TestResult;
20+
import io.qameta.allure.model.TestResultContainer;
2021
import org.junit.jupiter.api.Test;
2122
import org.junit.jupiter.api.io.TempDir;
2223

24+
import java.io.ByteArrayInputStream;
2325
import java.io.IOException;
26+
import java.io.InputStream;
27+
import java.nio.charset.StandardCharsets;
2428
import java.nio.file.Files;
2529
import java.nio.file.Path;
2630
import java.util.Arrays;
31+
import java.util.List;
2732
import java.util.UUID;
33+
import java.util.stream.Collectors;
34+
import java.util.stream.Stream;
2835

36+
import static io.qameta.allure.FileSystemResultsWriter.generateTestResultContainerName;
2937
import static io.qameta.allure.FileSystemResultsWriter.generateTestResultName;
3038
import static io.qameta.allure.test.ThreadLocalEnhancedRandom.current;
3139
import static org.assertj.core.api.Assertions.assertThat;
40+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
41+
3242
public class FileSystemResultsWriterTest {
3343

3444
@Test
@@ -54,6 +64,19 @@ void shouldWriteTestResult(@TempDir final Path folder) {
5464
.isRegularFile();
5565
}
5666

67+
@Test
68+
void shouldWriteTestResultContainer(@TempDir final Path folder) {
69+
FileSystemResultsWriter writer = new FileSystemResultsWriter(folder);
70+
final String uuid = UUID.randomUUID().toString();
71+
final TestResultContainer container = current().nextObject(TestResultContainer.class).setUuid(uuid);
72+
73+
writer.write(container);
74+
75+
final String fileName = generateTestResultContainerName(uuid);
76+
assertThat(folder.resolve(fileName))
77+
.isRegularFile();
78+
}
79+
5780
@Test
5881
void shouldWriteTitlePath(@TempDir final Path folder) throws IOException {
5982
FileSystemResultsWriter writer = new FileSystemResultsWriter(folder);
@@ -90,6 +113,35 @@ void shouldWriteStatusDetailsActualAndExpected(@TempDir final Path folder) throw
90113
.contains("\"expected\":\"expected value\"");
91114
}
92115

116+
@Test
117+
void shouldWriteAttachmentFile(@TempDir final Path folder) throws IOException {
118+
FileSystemResultsWriter writer = new FileSystemResultsWriter(folder);
119+
final String source = "source-attachment.txt";
120+
final String content = "attachment body";
121+
122+
writer.write(source, new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
123+
124+
assertThat(Files.readString(folder.resolve(source)))
125+
.isEqualTo(content);
126+
}
127+
128+
@Test
129+
void shouldNotCreateFinalAttachmentFileWhenStreamFails(@TempDir final Path folder) throws IOException {
130+
FileSystemResultsWriter writer = new FileSystemResultsWriter(folder);
131+
final String source = "broken-attachment.txt";
132+
final byte[] content = "partial attachment body".getBytes(StandardCharsets.UTF_8);
133+
134+
assertThatThrownBy(() -> writer.write(source, new FailingInputStream(content)))
135+
.isInstanceOf(AllureResultsWriteException.class)
136+
.hasMessage("Could not write Allure attachment")
137+
.hasCauseInstanceOf(IOException.class);
138+
139+
assertThat(folder.resolve(source))
140+
.doesNotExist();
141+
assertThat(listFiles(folder))
142+
.isEmpty();
143+
}
144+
93145
@Test
94146
void shouldPreserveOldResultsWhenCleanIsDisabled(@TempDir final Path folder) throws IOException {
95147
Path existingFile = folder.resolve("existing-result.json");
@@ -176,4 +228,30 @@ private static void writeTestResult(final FileSystemResultsWriter writer, final
176228
writer.write(testResult);
177229
});
178230
}
231+
232+
private static List<Path> listFiles(final Path folder) throws IOException {
233+
try (Stream<Path> files = Files.list(folder)) {
234+
return files.collect(Collectors.toList());
235+
}
236+
}
237+
238+
private static final class FailingInputStream extends InputStream {
239+
240+
private final byte[] content;
241+
242+
private int index;
243+
244+
private FailingInputStream(final byte[] content) {
245+
this.content = content;
246+
}
247+
248+
@Override
249+
public int read() throws IOException {
250+
if (index < content.length) {
251+
return content[index++] & 0xff;
252+
}
253+
throw new IOException("Simulated attachment stream failure");
254+
}
255+
}
256+
179257
}

0 commit comments

Comments
 (0)