8371182: [macos] Improve error messages for "invalid mac bundle identifier"

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-03-25 02:44:14 +00:00
parent 4408e1c927
commit 274f8e601c
15 changed files with 465 additions and 81 deletions

View File

@ -24,6 +24,8 @@
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.cli.StandardValidator.IS_VALID_MAC_BUNDLE_IDENTIFIER;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
@ -37,6 +39,7 @@ import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.AppImageSigningConfig;
import jdk.jpackage.internal.model.Application;
import jdk.jpackage.internal.model.ApplicationLaunchers;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.ExternalApplication;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.model.Launcher;
@ -138,20 +141,6 @@ final class MacApplicationBuilder {
return MacApplication.create(ApplicationBuilder.overrideAppImageLayout(app, appImageLayout), mixin);
}
static boolean isValidBundleIdentifier(String id) {
for (int i = 0; i < id.length(); i++) {
char a = id.charAt(i);
// We check for ASCII codes first which we accept. If check fails,
// check if it is acceptable extended ASCII or unicode character.
if ((a >= 'A' && a <= 'Z') || (a >= 'a' && a <= 'z')
|| (a >= '0' && a <= '9') || (a == '-' || a == '.')) {
continue;
}
return false;
}
return true;
}
private static void validateAppVersion(Application app) {
try {
CFBundleVersion.of(app.version());
@ -246,8 +235,8 @@ final class MacApplicationBuilder {
}
private String validatedBundleIdentifier(Application app) {
final var value = Optional.ofNullable(bundleIdentifier).orElseGet(() -> {
return app.mainLauncher()
return Optional.ofNullable(bundleIdentifier).orElseGet(() -> {
var derivedValue = app.mainLauncher()
.flatMap(Launcher::startupInfo)
.map(li -> {
final var packageName = li.packageName();
@ -258,15 +247,23 @@ final class MacApplicationBuilder {
}
})
.orElseGet(app::name);
if (!IS_VALID_MAC_BUNDLE_IDENTIFIER.test(derivedValue)) {
// Derived bundle identifier is invalid. Try to adjust it by dropping all invalid characters.
derivedValue = derivedValue.codePoints()
.mapToObj(Character::toString)
.filter(IS_VALID_MAC_BUNDLE_IDENTIFIER)
.collect(Collectors.joining(""));
if (!IS_VALID_MAC_BUNDLE_IDENTIFIER.test(derivedValue)) {
throw new ConfigException(
I18N.format("error.invalid-derived-bundle-identifier"),
I18N.format("error.invalid-derived-bundle-identifier.advice"));
}
}
Log.verbose(I18N.format("message.derived-bundle-identifier", derivedValue));
return derivedValue;
});
if (!isValidBundleIdentifier(value)) {
throw I18N.buildConfigException("message.invalid-identifier", value)
.advice("message.invalid-identifier.advice")
.create();
}
return value;
}
private String validatedCategory() {

View File

@ -33,6 +33,8 @@ error.invalid-app-image-runtime-image-bin-dir=Runtime directory {0} in the prede
error.invalid-runtime-image-bin-dir=Runtime image "{0}" should not contain "bin" folder
error.invalid-runtime-image-bin-dir.advice=Use --strip-native-commands jlink option when generating runtime image used with {0} option
error.invalid-app-image-plist-file=Invalid "{0}" file in the predefined application image
error.invalid-derived-bundle-identifier=Can't derive a valid bundle identifier from the input data
error.invalid-derived-bundle-identifier.advice=Specify bundle identifier with --mac-package-identifier option
resource.app-info-plist=Application Info.plist
resource.app-runtime-info-plist=Embedded Java Runtime Info.plist
@ -57,8 +59,7 @@ message.icon-not-icns= The specified icon "{0}" is not an ICNS file and will not
message.version-string-too-many-components='app-version' may have between 1 and 3 numbers: 1, 1.2, 1.2.3.
message.version-string-first-number-not-zero=The first number in an app-version cannot be zero or negative.
message.keychain.error=Unable to get keychain list.
message.invalid-identifier=Invalid mac bundle identifier [{0}].
message.invalid-identifier.advice=specify identifier with "--mac-package-identifier".
message.derived-bundle-identifier=Derived bundle identifier: {0}
message.preparing-dmg-setup=Preparing dmg setup: {0}.
message.preparing-scripts=Preparing package scripts.
message.preparing-distribution-dist=Preparing distribution.dist: {0}.

View File

@ -58,9 +58,11 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.cli.OptionValueExceptionFactory.StandardArgumentsMapper;
import jdk.jpackage.internal.model.AppImageBundleType;
import jdk.jpackage.internal.model.BundleType;
import jdk.jpackage.internal.model.BundlingOperationDescriptor;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.model.LauncherShortcut;
import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory;
@ -352,6 +354,11 @@ public final class StandardOption {
public static final OptionValue<String> MAC_BUNDLE_IDENTIFIER = stringOption("mac-package-identifier")
.valuePattern("package identifier")
.validator(StandardValidator.IS_VALID_MAC_BUNDLE_IDENTIFIER)
.validatorExceptionFactory(OptionValueExceptionFactory.build((message, cause) -> {
return new ConfigException(message, I18N.format("error.parameter-not-mac-bundle-identifier.advice"), cause);
}).formatArgumentsTransformer(StandardArgumentsMapper.VALUE_AND_NAME).create())
.validatorExceptionFormatString("error.parameter-not-mac-bundle-identifier")
.create();
public static final OptionValue<String> MAC_BUNDLE_SIGNING_PREFIX = stringOption("mac-package-signing-prefix").scope(MAC_SIGNING).create();

View File

@ -36,11 +36,12 @@ import java.nio.file.Path;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import jdk.jpackage.internal.cli.Validator.ValidatingConsumerException;
import jdk.jpackage.internal.util.FileUtils;
import jdk.jpackage.internal.util.MacBundle;
final public class StandardValidator {
public final class StandardValidator {
private StandardValidator() {
}
@ -143,6 +144,9 @@ final public class StandardValidator {
return MacBundle.fromPath(path).isPresent();
};
// https://developer.apple.com/documentation/BundleResources/Information-Property-List/CFBundleIdentifier
public static final Predicate<String> IS_VALID_MAC_BUNDLE_IDENTIFIER = Pattern.compile("[\\p{Alnum}-\\.]+").asMatchPredicate();
public static final class DirectoryListingIOException extends RuntimeException {

View File

@ -88,6 +88,8 @@ error.parameter-not-empty-directory=The value "{0}" provided for parameter {1} i
error.parameter-not-url=The value "{0}" provided for parameter {1} is not a valid URL
error.parameter-not-launcher-shortcut-dir=The value "{0}" provided for parameter {1} is not a valid shortcut startup directory
error.parameter-not-mac-bundle=The value "{0}" provided for parameter {1} is not a valid macOS bundle
error.parameter-not-mac-bundle-identifier=The value "{0}" provided for parameter {1} is not a valid macOS bundle identifier.
error.parameter-not-mac-bundle-identifier.advice=Bundle identifier must be a non-empty string containing only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.)
error.path-parameter-ioexception=I/O error accessing path value "{0}" of parameter {1}
error.parameter-add-launcher-malformed=The value "{0}" provided for parameter {1} does not match the pattern <name>=<file path>
error.parameter-add-launcher-not-file=The value of path to a property file "{0}" provided for additional launcher "{1}" is not a valid file path

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2026, 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
@ -27,6 +27,7 @@ import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public record CannedFormattedString(BiFunction<String, Object[], String> formatter, String key, List<Object> args) implements CannedArgument {
@ -67,6 +68,23 @@ public record CannedFormattedString(BiFunction<String, Object[], String> formatt
}
}
public interface Spec {
String format();
List<Object> modelArgs();
default CannedFormattedString asCannedFormattedString(Object ... args) {
if (args.length != modelArgs().size()) {
throw new IllegalArgumentException();
}
return JPackageStringBundle.MAIN.cannedFormattedString(format(), args);
}
default Pattern asPattern() {
return JPackageStringBundle.MAIN.cannedFormattedStringAsPattern(format(), modelArgs().toArray());
}
}
private record AddPrefixFormatter(BiFunction<String, Object[], String> formatter) implements BiFunction<String, Object[], String> {
AddPrefixFormatter {

View File

@ -1078,6 +1078,53 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return this;
}
public JPackageCommand validateOutput(
Class<? extends CannedFormattedString.Spec> messageGroup,
Consumer<JPackageOutputValidator> validatorMutator,
List<CannedFormattedString> expectedMessages) {
Objects.requireNonNull(validatorMutator);
if (!messageGroup.isEnum()) {
throw new IllegalArgumentException();
}
var messageSpecs = messageGroup.getEnumConstants();
var expectMessageFormats = expectedMessages.stream().map(CannedFormattedString::key).toList();
var groupMessageFormats = Stream.of(messageSpecs)
.map(CannedFormattedString.Spec::format)
.collect(Collectors.toMap(x -> x, x -> x))
.keySet();
if (!groupMessageFormats.containsAll(expectMessageFormats)) {
// Expected format strings should be a subset of the group format strings.
throw new IllegalArgumentException();
}
if (!expectedMessages.isEmpty()) {
new JPackageOutputValidator().expectMatchingStrings(expectedMessages).mutate(validatorMutator).applyTo(this);
}
Stream.of(messageSpecs).filter(spec -> {
return !expectMessageFormats.contains(spec.format());
}).map(CannedFormattedString.Spec::asPattern).map(pattern -> {
return TKit.assertTextStream(pattern).negate();
}).forEach(validator -> {
new JPackageOutputValidator().add(validator).stdoutAndStderr().applyTo(this);
});
return this;
}
public JPackageCommand validateOutput(
Class<? extends CannedFormattedString.Spec> messageGroup,
Consumer<JPackageOutputValidator> validatorMutator,
CannedFormattedString... expected) {
return validateOutput(messageGroup, validatorMutator, List.of(expected));
}
public boolean isWithToolProvider() {
return toolProviderSource.toolProvider().isPresent();
}

View File

@ -140,6 +140,11 @@ public final class JPackageOutputValidator {
return this;
}
public JPackageOutputValidator mutate(Consumer<JPackageOutputValidator> mutator) {
mutator.accept(this);
return this;
}
public void applyTo(JPackageCommand cmd) {
toResultConsumer(cmd).ifPresent(cmd::validateResult);
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2026, 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.
*/
package jdk.jpackage.internal;
import static jdk.jpackage.internal.MacPackagingPipeline.APPLICATION_LAYOUT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import jdk.jpackage.internal.model.ApplicationLaunchers;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.Launcher;
import jdk.jpackage.internal.model.LauncherStartupInfo;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class MacApplicationBuilderTest {
@ParameterizedTest
@CsvSource({
"NAME,!fo#o,foo",
"NAME,bar,bar",
"MAIN_LAUNCHER_CLASSNAME,foo.b$ar.Hello,foo.bar",
"MAIN_LAUNCHER_CLASSNAME,Hello$2,Hello2",
"NAME,!#,",
})
void testDerivedBundleIdentifier(ApplicationBuilderProperty type, String value, String expectedBundleIdentifier) {
var builder = buildApplication();
var macBuilder = new MacApplicationBuilder(builder);
type.applyTo(value, builder, macBuilder);
if (expectedBundleIdentifier != null) {
assertEquals(expectedBundleIdentifier, macBuilder.create().bundleIdentifier());
} else {
var ex = assertThrowsExactly(ConfigException.class, macBuilder::create);
assertEquals(I18N.format("error.invalid-derived-bundle-identifier"), ex.getMessage());
assertEquals(I18N.format("error.invalid-derived-bundle-identifier.advice"), ex.getAdvice());
}
}
private static ApplicationBuilder buildApplication() {
return new ApplicationBuilder().appImageLayout(APPLICATION_LAYOUT);
}
enum ApplicationBuilderProperty {
NAME {
void applyTo(String value, ApplicationBuilder builder, MacApplicationBuilder macBuilder) {
builder.name(Objects.requireNonNull(value));
}
},
MAIN_LAUNCHER_CLASSNAME {
void applyTo(String value, ApplicationBuilder builder, MacApplicationBuilder macBuilder) {
var startupInfo = new LauncherStartupInfo.Stub(Objects.requireNonNull(value), List.of(), List.of(), List.of());
var launcher = new Launcher.Stub(
startupInfo.simpleClassName(),
Optional.of(startupInfo),
List.of(),
false,
"",
Optional.empty(),
null,
Map.of());
builder.launchers(new ApplicationLaunchers(launcher));
}
}
;
abstract void applyTo(String value, ApplicationBuilder builder, MacApplicationBuilder macBuilder);
}
}

View File

@ -63,3 +63,11 @@
* jdk/jpackage/internal/ActiveKeychainListTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.ActiveKeychainListTest
*/
/* @test
* @summary Test MacApplicationBuilderTest
* @requires (os.family == "mac")
* @compile/module=jdk.jpackage -Xlint:all -Werror
* jdk/jpackage/internal/MacApplicationBuilderTest.java
* @run junit jdk.jpackage/jdk.jpackage.internal.MacApplicationBuilderTest
*/

View File

@ -18,7 +18,6 @@ ErrorTest.test(IMAGE; args-add=[--module, com.foo.bar, --runtime-image, @@JAVA_H
ErrorTest.test(IMAGE; args-add=[--module, java.base, --runtime-image, @@JAVA_HOME@@]; errors=[message.error-header+[ERR_NoMainClass]])
ErrorTest.test(LINUX_DEB; app-desc=Hello; args-add=[--linux-package-name, #]; errors=[message.error-header+[error.deb-invalid-value-for-package-name, #], message.advice-header+[error.deb-invalid-value-for-package-name.advice]])
ErrorTest.test(LINUX_RPM; app-desc=Hello; args-add=[--linux-package-name, #]; errors=[message.error-header+[error.rpm-invalid-value-for-package-name, #], message.advice-header+[error.rpm-invalid-value-for-package-name.advice]])
ErrorTest.test(MAC_PKG; app-desc=Hello; args-add=[--mac-package-identifier, #1]; errors=[message.error-header+[message.invalid-identifier, #1], message.advice-header+[message.invalid-identifier.advice]])
ErrorTest.test(NATIVE; app-desc=Hello; args-add=[--mac-app-store, --runtime-image, @@JAVA_HOME@@]; errors=[message.error-header+[error.invalid-runtime-image-bin-dir, @@JAVA_HOME@@], message.advice-header+[error.invalid-runtime-image-bin-dir.advice, --mac-app-store]])
ErrorTest.test(NATIVE; app-desc=Hello; args-add=[--runtime-image, @@EMPTY_DIR@@]; errors=[message.error-header+[error.invalid-runtime-image-missing-file, @@EMPTY_DIR@@, lib/**/libjli.dylib]])
ErrorTest.test(NATIVE; app-desc=Hello; args-add=[--runtime-image, @@INVALID_MAC_RUNTIME_BUNDLE@@]; errors=[message.error-header+[error.invalid-runtime-image-missing-file, @@INVALID_MAC_RUNTIME_BUNDLE@@, Contents/Home/lib/**/libjli.dylib]])

View File

@ -58,6 +58,7 @@ import jdk.jpackage.internal.cli.StandardOption.LauncherProperty;
import jdk.jpackage.internal.model.AppImageBundleType;
import jdk.jpackage.internal.model.BundleType;
import jdk.jpackage.internal.model.JPackageException;
import jdk.jpackage.internal.model.ConfigException;
import jdk.jpackage.internal.model.LauncherShortcut;
import jdk.jpackage.internal.model.LauncherShortcutStartupDirectory;
import jdk.jpackage.internal.util.RootedPath;
@ -71,6 +72,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
@ -258,6 +260,40 @@ public class StandardOptionTest extends JUnitAdapter.TestSrcInitializer {
}).toList(), result.errors());
}
@ParameterizedTest
@ValueSource(strings = {
".",
"a-b.c",
"A",
"com.acme.Foo"
})
void test_MAC_BUNDLE_IDENTIFIER_valid(String id) {
var spec = StandardOption.MAC_BUNDLE_IDENTIFIER.getSpec();
var result = spec.convert(spec.name(), StringToken.of(id)).orElseThrow();
assertEquals(result, id);
}
@ParameterizedTest
@ValueSource(strings = {
"",
",",
"Hello!"
})
void test_MAC_BUNDLE_IDENTIFIER_invalid(String id) {
var spec = StandardOption.MAC_BUNDLE_IDENTIFIER.getSpec();
var result = spec.convert(spec.name(), StringToken.of(id));
var ex = assertThrows(ConfigException.class, result::orElseThrow);
assertEquals(I18N.format("error.parameter-not-mac-bundle-identifier", id, spec.name().formatForCommandLine()), ex.getMessage());
assertEquals(I18N.format("error.parameter-not-mac-bundle-identifier.advice"), ex.getAdvice());
}
@ParameterizedTest
@EnumSource(OptionMutatorTest.TestType.class)
public void test_pathOptionMutator(OptionMutatorTest.TestType type) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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
@ -21,11 +21,20 @@
* questions.
*/
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.MacHelper;
import jdk.jpackage.test.JPackageCommand;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.CannedFormattedString;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JPackageOutputValidator;
import jdk.jpackage.test.MacHelper;
import jdk.jpackage.test.TKit;
/**
@ -43,30 +52,198 @@ import jdk.jpackage.test.Annotations.Parameter;
* --jpt-run=MacPropertiesTest
*/
public class MacPropertiesTest {
@Test
@Parameter("MacPackageNameTest")
public void testPackageName(String packageName) {
testParameterInAppImage("--mac-package-name", "CFBundleName",
packageName);
}
@Test
@Parameter("Foo")
public void testPackageIdetifier(String packageId) {
testParameterInAppImage("--mac-package-identifier", "CFBundleIdentifier",
packageId);
@ParameterSupplier
public void test(TestSpec spec) {
spec.run();
}
private static void testParameterInAppImage(String jpackageParameterName,
String plistKeyName, String value) {
JPackageCommand cmd = JPackageCommand.helloAppImage()
.addArguments(jpackageParameterName, value);
public static Collection<Object[]> test() {
cmd.executeAndAssertHelloAppImageCreated();
var testCases = new ArrayList<TestSpec.Builder>();
var plist = MacHelper.readPListFromAppImage(cmd.outputBundle());
testCases.addAll(List.of(
TestSpec.build("CFBundleName").addArgs("--mac-package-name", "MacPackageNameTest").expect("MacPackageNameTest"),
TestSpec.build("CFBundleIdentifier").addArgs("--mac-package-identifier", "Foo").expect("Foo"),
// Should derive from the input data.
TestSpec.build("CFBundleIdentifier").appDesc("com.acme.Hello").expect("com.acme").expect(BundleIdentifierMessage.VALUE.asCannedFormattedString("com.acme"))
));
TKit.assertEquals(value, plist.queryValue(plistKeyName), String.format(
"Check value of %s plist key", plistKeyName));
return testCases.stream().map(TestSpec.Builder::create).map(v -> {
return new Object[] {v};
}).toList();
}
enum BundleIdentifierMessage implements CannedFormattedString.Spec {
VALUE("message.derived-bundle-identifier", "bundle-id"),
;
BundleIdentifierMessage(String key, Object ... args) {
this.key = Objects.requireNonNull(key);
this.args = List.of(args);
}
@Override
public String format() {
return key;
}
@Override
public List<Object> modelArgs() {
return args;
}
private final String key;
private final List<Object> args;
}
record TestSpec(
Optional<String> appDesc,
List<String> addArgs,
List<String> delArgs,
List<CannedFormattedString> expectedTraceMessages,
String expectedInfoPlistKeyValue,
String infoPlistKey,
Optional<Class<? extends CannedFormattedString.Spec>> traceMessagesClass) {
TestSpec {
Objects.requireNonNull(addArgs);
Objects.requireNonNull(delArgs);
Objects.requireNonNull(expectedTraceMessages);
Objects.requireNonNull(expectedInfoPlistKeyValue);
Objects.requireNonNull(infoPlistKey);
Objects.requireNonNull(traceMessagesClass);
}
void run() {
var cmd = appDesc.map(JPackageCommand::helloAppImage).orElseGet(JPackageCommand::helloAppImage)
.setFakeRuntime().addArguments(addArgs);
delArgs.forEach(cmd::removeArgumentWithValue);
Consumer<JPackageOutputValidator> validatorMutator = validator -> {
validator.matchTimestamps().stripTimestamps();
};
traceMessagesClass.ifPresentOrElse(v -> {
cmd.validateOutput(v, validatorMutator, expectedTraceMessages);
}, () -> {
new JPackageOutputValidator()
.mutate(validatorMutator)
.expectMatchingStrings(expectedTraceMessages)
.applyTo(cmd);
});
cmd.executeAndAssertHelloAppImageCreated();
var plist = MacHelper.readPListFromAppImage(cmd.outputBundle());
TKit.assertEquals(
expectedInfoPlistKeyValue,
plist.queryValue(infoPlistKey),
String.format("Check value of %s plist key", infoPlistKey));
}
@Override
public String toString() {
var tokens = new ArrayList<String>();
tokens.add(String.format("%s=>%s", infoPlistKey, expectedInfoPlistKeyValue));
appDesc.ifPresent(v -> {
tokens.add("app-desc=" + v);
});
if (!addArgs.isEmpty()) {
tokens.add("args-add=" + addArgs);
}
if (!delArgs.isEmpty()) {
tokens.add("args-del=" + delArgs);
}
if (!expectedTraceMessages.isEmpty()) {
tokens.add("expect=" + expectedTraceMessages);
}
return tokens.stream().collect(Collectors.joining("; "));
}
static Builder build() {
return new Builder();
}
static Builder build(String infoPlistKey) {
return build().infoPlistKey(Objects.requireNonNull(infoPlistKey));
}
static final class Builder {
TestSpec create() {
Class<? extends CannedFormattedString.Spec> traceMessagesClass = switch (Objects.requireNonNull(infoPlistKey)) {
case "CFBundleIdentifier" -> BundleIdentifierMessage.class;
case "CFBundleName" -> null;
default -> {
throw new IllegalStateException();
}
};
return new TestSpec(
Optional.ofNullable(appDesc),
addArgs,
delArgs,
expectedTraceMessages,
expectedInfoPlistKeyValue,
infoPlistKey,
Optional.ofNullable(traceMessagesClass));
}
Builder appDesc(String v) {
appDesc = v;
return this;
}
Builder addArgs(List<String> v) {
addArgs.addAll(v);
return this;
}
Builder addArgs(String... args) {
return addArgs(List.of(args));
}
Builder delArgs(List<String> v) {
delArgs.addAll(v);
return this;
}
Builder delArgs(String... args) {
return delArgs(List.of(args));
}
Builder expect(CannedFormattedString traceMessage) {
expectedTraceMessages.add(traceMessage);
return this;
}
Builder expect(String v) {
expectedInfoPlistKeyValue = v;
return this;
}
Builder infoPlistKey(String v) {
infoPlistKey = v;
return this;
}
private String appDesc;
private final List<String> addArgs = new ArrayList<>();
private final List<String> delArgs = new ArrayList<>();
private final List<CannedFormattedString> expectedTraceMessages = new ArrayList<>();
private String expectedInfoPlistKeyValue;
private String infoPlistKey;
}
}
}

View File

@ -46,7 +46,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.util.MacBundle;
import jdk.jpackage.internal.util.RuntimeReleaseFile;
@ -58,8 +57,6 @@ import jdk.jpackage.test.CannedFormattedString;
import jdk.jpackage.test.ConfigurationTarget;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JPackageCommand.StandardAssert;
import jdk.jpackage.test.JPackageOutputValidator;
import jdk.jpackage.test.JPackageStringBundle;
import jdk.jpackage.test.JavaAppDesc;
import jdk.jpackage.test.MacHelper;
import jdk.jpackage.test.PackageTest;
@ -270,7 +267,7 @@ public final class AppVersionTest {
}).toList();
}
enum Message {
enum Message implements CannedFormattedString.Spec {
VERSION_FROM_MODULE("message.module-version", "version", "module"),
VERSION_FROM_RELEASE_FILE("message.release-version", "version"),
VERSION_NORMALIZED("message.version-normalized", "version", "version"),
@ -278,24 +275,21 @@ public final class AppVersionTest {
Message(String key, Object ... args) {
this.key = Objects.requireNonNull(key);
this.args = args;
this.args = List.of(args);
}
CannedFormattedString cannedFormattedString(Object ... args) {
return JPackageStringBundle.MAIN.cannedFormattedString(key, args);
}
TKit.TextStreamVerifier negateFind() {
var pattern = JPackageStringBundle.MAIN.cannedFormattedStringAsPattern(key, args);
return TKit.assertTextStream(pattern).negate();
}
String key() {
@Override
public String format() {
return key;
}
@Override
public List<Object> modelArgs() {
return args;
}
private final String key;
private final Object[] args;
private final List<Object> args;
}
sealed interface VersionSource {
@ -389,16 +383,9 @@ public final class AppVersionTest {
}
void applyTo(JPackageCommand cmd) {
Objects.requireNonNull(cmd);
new JPackageOutputValidator().expectMatchingStrings(messages).matchTimestamps().stripTimestamps().applyTo(cmd);
cmd.version(version);
var expectMessageKeys = messages.stream().map(CannedFormattedString::key).toList();
Stream.of(Message.values()).filter(message -> {
return !expectMessageKeys.contains(message.key());
}).map(Message::negateFind).forEach(validator -> {
new JPackageOutputValidator().add(validator).stdoutAndStderr().applyTo(cmd);
});
cmd.version(version).validateOutput(Message.class, validator -> {
validator.matchTimestamps().stripTimestamps();
}, messages);
}
@Override
@ -436,7 +423,7 @@ public final class AppVersionTest {
}
Builder message(Message message, Object ... args) {
return messages(message.cannedFormattedString(args));
return messages(message.asCannedFormattedString(args));
}
private String version;

View File

@ -1015,8 +1015,8 @@ public final class ErrorTest {
testSpec().noAppDesc().addArgs("--app-image", Token.APP_IMAGE.token())
.error("error.app-image.mac-sign.required"),
testSpec().type(PackageType.MAC_PKG).addArgs("--mac-package-identifier", "#1")
.error("message.invalid-identifier", "#1")
.advice("message.invalid-identifier.advice"),
.error("error.parameter-not-mac-bundle-identifier", "#1", "--mac-package-identifier")
.advice("error.parameter-not-mac-bundle-identifier.advice"),
// Bundle for mac app store should not have runtime commands
testSpec().nativeType().addArgs("--mac-app-store", "--jlink-options", "--bind-services")
.error("ERR_MissingJLinkOptMacAppStore", "--strip-native-commands"),