db85090553
Co-authored-by: Sean Mullan <mullan@openjdk.org> Co-authored-by: Alan Bateman <alanb@openjdk.org> Co-authored-by: Weijun Wang <weijun@openjdk.org> Co-authored-by: Aleksei Efimov <aefimov@openjdk.org> Co-authored-by: Brian Burkhalter <bpb@openjdk.org> Co-authored-by: Daniel Fuchs <dfuchs@openjdk.org> Co-authored-by: Harshitha Onkar <honkar@openjdk.org> Co-authored-by: Joe Wang <joehw@openjdk.org> Co-authored-by: Jorn Vernee <jvernee@openjdk.org> Co-authored-by: Justin Lu <jlu@openjdk.org> Co-authored-by: Kevin Walls <kevinw@openjdk.org> Co-authored-by: Lance Andersen <lancea@openjdk.org> Co-authored-by: Naoto Sato <naoto@openjdk.org> Co-authored-by: Roger Riggs <rriggs@openjdk.org> Co-authored-by: Brent Christian <bchristi@openjdk.org> Co-authored-by: Stuart Marks <smarks@openjdk.org> Co-authored-by: Ian Graves <igraves@openjdk.org> Co-authored-by: Phil Race <prr@openjdk.org> Co-authored-by: Erik Gahlin <egahlin@openjdk.org> Co-authored-by: Jaikiran Pai <jpai@openjdk.org> Reviewed-by: kevinw, aivanov, rriggs, lancea, coffeys, dfuchs, ihse, erikj, cjplummer, coleenp, naoto, mchung, prr, weijun, joehw, azvegint, psadhukhan, bchristi, sundar, attila
408 lines
20 KiB
Java
408 lines
20 KiB
Java
/*
|
|
* Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
import jdk.test.lib.process.OutputAnalyzer;
|
|
import jdk.test.lib.process.ProcessTools;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.time.Instant;
|
|
import java.time.ZoneOffset;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.format.DateTimeParseException;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Properties;
|
|
import java.util.TimeZone;
|
|
|
|
/*
|
|
* @test
|
|
* @summary Tests that the Properties.store() APIs generate output that is reproducible
|
|
* @bug 8231640 8282023 8316540
|
|
* @comment The test launches several processes and in the presence of -Xcomp it's too slow
|
|
* and thus causes timeouts
|
|
* @requires vm.compMode != "Xcomp"
|
|
* @library /test/lib
|
|
* @run driver StoreReproducibilityTest
|
|
*/
|
|
public class StoreReproducibilityTest {
|
|
|
|
private static final String DATE_FORMAT_PATTERN = "EEE MMM dd HH:mm:ss zzz uuuu";
|
|
private static final String SYS_PROP_JAVA_PROPERTIES_DATE = "java.properties.date";
|
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN, Locale.ROOT)
|
|
.withZone(ZoneOffset.UTC);
|
|
|
|
public static void main(final String[] args) throws Exception {
|
|
// test that store method uses the java.properties.date system property
|
|
testStoreUsesPropValue();
|
|
// free form non-date value for java.properties.date system property
|
|
testNonDateSysPropValue();
|
|
// blank value for java.properties.date system property
|
|
testBlankSysPropValue();
|
|
// empty value for java.properties.date system property
|
|
testEmptySysPropValue();
|
|
// value for java.properties.date system property contains line terminator characters
|
|
testMultiLineSysPropValue();
|
|
// value for java.properties.date system property contains backslash character
|
|
testBackSlashInSysPropValue();
|
|
}
|
|
|
|
/**
|
|
* Launches a Java program which is responsible for using Properties.store() to write out the
|
|
* properties to a file. The launched Java program is passed a value for the
|
|
* {@code java.properties.date} system property and the date comment written out
|
|
* to the file is expected to use this value.
|
|
* The program is launched multiple times with the same value for {@code java.properties.date}
|
|
* and the output written by each run of this program is verified to be exactly the same.
|
|
* Additionally, the date comment that's written out is verified to be the expected date that
|
|
* corresponds to the passed {@code java.properties.date}.
|
|
*/
|
|
private static void testStoreUsesPropValue() throws Exception {
|
|
final List<Path> storedFiles = new ArrayList<>();
|
|
final String sysPropVal = FORMATTER.format(Instant.ofEpochSecond(243535322));
|
|
for (int i = 0; i < 5; i++) {
|
|
final Path tmpFile = Files.createTempFile("8231640", ".props");
|
|
storedFiles.add(tmpFile);
|
|
final ProcessBuilder processBuilder = ProcessTools.createTestJavaProcessBuilder(
|
|
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
|
|
StoreTest.class.getName(),
|
|
tmpFile.toString(),
|
|
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
|
|
executeJavaProcess(processBuilder);
|
|
assertExpectedComment(tmpFile, sysPropVal);
|
|
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
|
|
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
|
|
}
|
|
}
|
|
assertAllFileContentsAreSame(storedFiles, sysPropVal);
|
|
}
|
|
|
|
/**
|
|
* Launches a Java program which is responsible for using Properties.store() to write out the
|
|
* properties to a file. The launched Java program is passed a {@link String#isBlank() blank} value
|
|
* for the {@code java.properties.date} system property.
|
|
* It is expected and verified in this test that such a value for the system property
|
|
* will cause a comment line to be written out with only whitespaces.
|
|
* The launched program is expected to complete without any errors.
|
|
*/
|
|
private static void testBlankSysPropValue() throws Exception {
|
|
final List<Path> storedFiles = new ArrayList<>();
|
|
final String sysPropVal = " \t";
|
|
for (int i = 0; i < 2; i++) {
|
|
final Path tmpFile = Files.createTempFile("8231640", ".props");
|
|
storedFiles.add(tmpFile);
|
|
final ProcessBuilder processBuilder = ProcessTools.createTestJavaProcessBuilder(
|
|
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
|
|
StoreTest.class.getName(),
|
|
tmpFile.toString(),
|
|
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
|
|
executeJavaProcess(processBuilder);
|
|
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
|
|
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
|
|
}
|
|
String blankCommentLine = findNthComment(tmpFile, 2);
|
|
if (blankCommentLine == null) {
|
|
throw new RuntimeException("Comment line representing the value of "
|
|
+ SYS_PROP_JAVA_PROPERTIES_DATE + " system property is missing in file " + tmpFile);
|
|
}
|
|
if (!blankCommentLine.isBlank()) {
|
|
throw new RuntimeException("Expected comment line to be blank but was " + blankCommentLine);
|
|
}
|
|
}
|
|
assertAllFileContentsAreSame(storedFiles, sysPropVal);
|
|
}
|
|
|
|
/**
|
|
* Launches a Java program which is responsible for using Properties.store() to write out the
|
|
* properties to a file. The launched Java program is passed a {@link String#isEmpty() empty} value
|
|
* for the {@code java.properties.date} system property.
|
|
* It is expected and verified in this test that such a value for the system property
|
|
* will cause the current date and time to be written out as a comment.
|
|
* The launched program is expected to complete without any errors.
|
|
*/
|
|
private static void testEmptySysPropValue() throws Exception {
|
|
for (int i = 0; i < 2; i++) {
|
|
final Path tmpFile = Files.createTempFile("8231640", ".props");
|
|
final ProcessBuilder processBuilder = ProcessTools.createTestJavaProcessBuilder(
|
|
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=",
|
|
"-Duser.timezone=UTC",
|
|
StoreTest.class.getName(),
|
|
tmpFile.toString(),
|
|
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
|
|
Date launchedAt = new Date();
|
|
// wait for a second before launching so that we can then expect
|
|
// the date written out by the store() APIs to be "after" this launch date
|
|
Thread.sleep(1000);
|
|
executeJavaProcess(processBuilder);
|
|
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
|
|
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
|
|
}
|
|
assertCurrentDate(tmpFile, launchedAt);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Launches a Java program which is responsible for using Properties.store() to write out the
|
|
* properties to a file. The launched Java program is passed the {@code java.properties.date}
|
|
* system property with a value that doesn't represent a formatted date.
|
|
* It is expected and verified in this test that such a value for the system property
|
|
* will cause the comment to use that value verbatim. The launched program is expected to complete
|
|
* without any errors.
|
|
*/
|
|
private static void testNonDateSysPropValue() throws Exception {
|
|
final String sysPropVal = "foo-bar";
|
|
final List<Path> storedFiles = new ArrayList<>();
|
|
for (int i = 0; i < 2; i++) {
|
|
final Path tmpFile = Files.createTempFile("8231640", ".props");
|
|
storedFiles.add(tmpFile);
|
|
final ProcessBuilder processBuilder = ProcessTools.createTestJavaProcessBuilder(
|
|
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
|
|
StoreTest.class.getName(),
|
|
tmpFile.toString(),
|
|
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
|
|
executeJavaProcess(processBuilder);
|
|
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
|
|
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
|
|
}
|
|
assertExpectedComment(tmpFile, sysPropVal);
|
|
}
|
|
assertAllFileContentsAreSame(storedFiles, sysPropVal);
|
|
}
|
|
|
|
/**
|
|
* Launches a Java program which is responsible for using Properties.store() to write out the
|
|
* properties to a file. The launched Java program is passed the {@code java.properties.date}
|
|
* system property with a value that has line terminator characters.
|
|
* It is expected and verified in this test that such a value for the system property
|
|
* will cause the comment written out to be multiple separate comments. The launched program is expected
|
|
* to complete without any errors.
|
|
*/
|
|
private static void testMultiLineSysPropValue() throws Exception {
|
|
final String[] sysPropVals = {"hello-world\nc=d", "hello-world\rc=d", "hello-world\r\nc=d"};
|
|
for (final String sysPropVal : sysPropVals) {
|
|
final List<Path> storedFiles = new ArrayList<>();
|
|
for (int i = 0; i < 2; i++) {
|
|
final Path tmpFile = Files.createTempFile("8231640", ".props");
|
|
storedFiles.add(tmpFile);
|
|
final ProcessBuilder processBuilder = ProcessTools.createTestJavaProcessBuilder(
|
|
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
|
|
StoreTest.class.getName(),
|
|
tmpFile.toString(),
|
|
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
|
|
executeJavaProcess(processBuilder);
|
|
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
|
|
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
|
|
}
|
|
// verify this results in 2 separate comment lines in the stored file
|
|
String commentLine1 = findNthComment(tmpFile, 2);
|
|
String commentLine2 = findNthComment(tmpFile, 3);
|
|
if (commentLine1 == null || commentLine2 == null) {
|
|
throw new RuntimeException("Did not find the expected multi-line comments in " + tmpFile);
|
|
}
|
|
if (!commentLine1.equals("hello-world")) {
|
|
throw new RuntimeException("Unexpected comment line " + commentLine1 + " in " + tmpFile);
|
|
}
|
|
if (!commentLine2.equals("c=d")) {
|
|
throw new RuntimeException("Unexpected comment line " + commentLine2 + " in " + tmpFile);
|
|
}
|
|
}
|
|
assertAllFileContentsAreSame(storedFiles, sysPropVal);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Launches a Java program which is responsible for using Properties.store() to write out the
|
|
* properties to a file. The launched Java program is passed the {@code java.properties.date}
|
|
* system property with a value that has backslash character.
|
|
* It is expected and verified in this test that such a value for the system property
|
|
* will not cause any malformed comments or introduce any new properties in the stored content.
|
|
* The launched program is expected to complete without any errors.
|
|
*/
|
|
private static void testBackSlashInSysPropValue() throws Exception {
|
|
final String[] sysPropVals = {"\\hello-world", "hello-world\\", "hello-world\\c=d",
|
|
"newline-plus-backslash\\\nc=d"};
|
|
for (final String sysPropVal : sysPropVals) {
|
|
final List<Path> storedFiles = new ArrayList<>();
|
|
for (int i = 0; i < 2; i++) {
|
|
final Path tmpFile = Files.createTempFile("8231640", ".props");
|
|
storedFiles.add(tmpFile);
|
|
final ProcessBuilder processBuilder = ProcessTools.createTestJavaProcessBuilder(
|
|
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
|
|
StoreTest.class.getName(),
|
|
tmpFile.toString(),
|
|
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
|
|
executeJavaProcess(processBuilder);
|
|
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
|
|
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
|
|
}
|
|
String commentLine1 = findNthComment(tmpFile, 2);
|
|
if (commentLine1 == null) {
|
|
throw new RuntimeException("Did not find the expected comment line in " + tmpFile);
|
|
}
|
|
if (sysPropVal.contains("newline-plus-backslash")) {
|
|
if (!commentLine1.equals("newline-plus-backslash\\")) {
|
|
throw new RuntimeException("Unexpected comment line " + commentLine1 + " in " + tmpFile);
|
|
}
|
|
// we expect this specific system property value to be written out into 2 separate comment lines
|
|
String commentLine2 = findNthComment(tmpFile, 3);
|
|
if (commentLine2 == null) {
|
|
throw new RuntimeException(sysPropVal + " was expected to be split into 2 comment line, " +
|
|
"but wasn't, in " + tmpFile);
|
|
}
|
|
if (!commentLine2.equals("c=d")) {
|
|
throw new RuntimeException("Unexpected comment line " + commentLine2 + " in " + tmpFile);
|
|
}
|
|
} else {
|
|
if (!commentLine1.equals(sysPropVal)) {
|
|
throw new RuntimeException("Unexpected comment line " + commentLine1 + " in " + tmpFile);
|
|
}
|
|
}
|
|
}
|
|
assertAllFileContentsAreSame(storedFiles, sysPropVal);
|
|
}
|
|
}
|
|
|
|
// launches the java process and waits for it to exit. throws an exception if exit value is non-zero
|
|
private static void executeJavaProcess(ProcessBuilder pb) throws Exception {
|
|
final OutputAnalyzer outputAnalyzer = ProcessTools.executeProcess(pb);
|
|
try {
|
|
outputAnalyzer.shouldHaveExitValue(0);
|
|
} finally {
|
|
// print out any stdout/err that was generated in the launched program
|
|
outputAnalyzer.reportDiagnosticSummary();
|
|
}
|
|
}
|
|
|
|
// Properties.load() from the passed file and return the loaded Properties instance
|
|
private static Properties loadProperties(final Path file) throws IOException {
|
|
final Properties props = new Properties();
|
|
props.load(Files.newBufferedReader(file));
|
|
return props;
|
|
}
|
|
|
|
/**
|
|
* Verifies that the comment in the {@code destFile} is same as {@code expectedComment},
|
|
* instead of the default date comment.
|
|
*/
|
|
private static void assertExpectedComment(final Path destFile,
|
|
final String expectedComment) throws Exception {
|
|
final String actualComment = findNthComment(destFile, 2);
|
|
if (actualComment == null) {
|
|
throw new RuntimeException("Comment \"" + expectedComment + "\" not found in stored properties " + destFile);
|
|
}
|
|
if (!expectedComment.equals(actualComment)) {
|
|
throw new RuntimeException("Expected comment \"" + expectedComment + "\" but found \"" + actualComment + "\" " +
|
|
"in stored properties " + destFile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifies that the date comment in the {@code destFile} can be parsed using the
|
|
* "EEE MMM dd HH:mm:ss zzz uuuu" format and the time represented by it is {@link Date#after(Date) after}
|
|
* the passed {@code date}
|
|
* The JVM runtime to invoke this method should set the time zone to UTC, i.e, specify
|
|
* "-Duser.timezone=UTC" at the command line. Otherwise, it will fail with some time
|
|
* zones that have ambiguous short names, such as "IST"
|
|
*/
|
|
private static void assertCurrentDate(final Path destFile, final Date date) throws Exception {
|
|
final String dateComment = findNthComment(destFile, 2);
|
|
if (dateComment == null) {
|
|
throw new RuntimeException("Date comment not found in stored properties " + destFile);
|
|
}
|
|
System.out.println("Found date comment " + dateComment + " in file " + destFile);
|
|
final Date parsedDate;
|
|
try {
|
|
Instant instant = Instant.from(FORMATTER.parse(dateComment));
|
|
parsedDate = new Date(instant.toEpochMilli());
|
|
} catch (DateTimeParseException pe) {
|
|
throw new RuntimeException("Unexpected date " + dateComment + " in stored properties " + destFile, pe);
|
|
}
|
|
if (!parsedDate.after(date)) {
|
|
throw new RuntimeException("Expected date comment " + dateComment + " to be after " + date
|
|
+ " but was " + parsedDate);
|
|
}
|
|
}
|
|
|
|
// returns the "Nth" comment from the file. Comment index starts from 1.
|
|
private static String findNthComment(Path file, int commentIndex) throws IOException {
|
|
List<String> comments = new ArrayList<>();
|
|
try (final BufferedReader reader = Files.newBufferedReader(file)) {
|
|
String line;
|
|
while ((line = reader.readLine()) != null) {
|
|
if (line.startsWith("#")) {
|
|
comments.add(line.substring(1));
|
|
if (comments.size() == commentIndex) {
|
|
return comments.get(commentIndex - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// verifies the byte equality of the contents in each of the files
|
|
private static void assertAllFileContentsAreSame(final List<Path> files,
|
|
final String sysPropVal) throws Exception {
|
|
final byte[] file1Contents = Files.readAllBytes(files.get(0));
|
|
for (int i = 1; i < files.size(); i++) {
|
|
final byte[] otherFileContents = Files.readAllBytes(files.get(i));
|
|
if (!Arrays.equals(file1Contents, otherFileContents)) {
|
|
throw new RuntimeException("Properties.store() did not generate reproducible content when " +
|
|
SYS_PROP_JAVA_PROPERTIES_DATE + " was set to " + sysPropVal);
|
|
}
|
|
}
|
|
}
|
|
|
|
static class StoreTest {
|
|
private static final Properties propsToStore = new Properties();
|
|
|
|
static {
|
|
propsToStore.setProperty("a", "b");
|
|
}
|
|
|
|
/**
|
|
* Uses Properties.store() APIs to store the properties into file
|
|
*/
|
|
public static void main(final String[] args) throws Exception {
|
|
final Path destFile = Path.of(args[0]);
|
|
final String comment = "some user specified comment";
|
|
System.out.println("Current default timezone is " + TimeZone.getDefault());
|
|
if (args[1].equals("--use-outputstream")) {
|
|
try (var os = Files.newOutputStream(destFile)) {
|
|
propsToStore.store(os, comment);
|
|
}
|
|
} else {
|
|
try (var br = Files.newBufferedWriter(destFile)) {
|
|
propsToStore.store(br, comment);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|