mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-11 11:45:27 +02:00
Select the operator profile at startup
Resolves the active operator profile before the chat controller is built and passes its two file names on, so preferences, layout and worked data follow the profile. The resolution is deliberately quiet for existing installations. With no registry or exactly one profile nothing is asked and nothing is written, so a single operator start is unchanged. Only from two profiles on does a small picker appear with the last used profile preselected, where Enter or a double click starts immediately. A "--profile" argument, or the equivalent system property, skips the picker; an unknown name warns and falls back to the normal selection instead of refusing to start. The startup decision itself lives in OperatorProfileBootstrap and contains no user interface code, so it is covered by headless tests. The window title gains the profile name only when a second profile exists. Command line parsing happens in init() and is kept in a process wide holder, because JavaFX only knows the parameters of the instance it launched itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hpa6bjie5qkeNG62y6FmXm
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Command line options of the application.
|
||||
*
|
||||
* <p>The parsed value is additionally kept in a process wide holder. JavaFX only knows
|
||||
* the parameters of the {@code Application} instance it launched itself, so an instance
|
||||
* created during a profile switch would see no parameters at all. Parsing once at
|
||||
* startup and remembering the result avoids that entirely.</p>
|
||||
*/
|
||||
public class CommandLineOptions {
|
||||
|
||||
/**
|
||||
* Command line switch selecting the operator profile to start with.
|
||||
*/
|
||||
public static final String PROFILE_ARGUMENT = "--profile";
|
||||
|
||||
/**
|
||||
* System property used as an alternative to the command line switch.
|
||||
*/
|
||||
public static final String PROFILE_SYSTEM_PROPERTY = "kst4contest.profile";
|
||||
|
||||
private static volatile CommandLineOptions rememberedOptions = new CommandLineOptions(null);
|
||||
|
||||
private final String requestedProfileName;
|
||||
|
||||
public CommandLineOptions(final String requestedProfileName) {
|
||||
this.requestedProfileName = requestedProfileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the raw application arguments.
|
||||
*
|
||||
* <p>Unknown arguments are ignored on purpose. A typo in a command line must never
|
||||
* keep an operator out of the application shortly before a contest.</p>
|
||||
*
|
||||
* @param rawArguments raw arguments, may be null
|
||||
* @return the parsed options
|
||||
*/
|
||||
public static CommandLineOptions parse(final List<String> rawArguments) {
|
||||
|
||||
String requestedProfileName = null;
|
||||
|
||||
if (rawArguments != null) {
|
||||
for (int argumentIndex = 0; argumentIndex < rawArguments.size(); argumentIndex++) {
|
||||
String currentArgument = rawArguments.get(argumentIndex);
|
||||
|
||||
if (currentArgument == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentArgument.startsWith(PROFILE_ARGUMENT + "=")) {
|
||||
requestedProfileName = currentArgument.substring(PROFILE_ARGUMENT.length() + 1);
|
||||
} else if (PROFILE_ARGUMENT.equals(currentArgument)
|
||||
&& argumentIndex + 1 < rawArguments.size()) {
|
||||
requestedProfileName = rawArguments.get(argumentIndex + 1);
|
||||
argumentIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestedProfileName == null || requestedProfileName.isBlank()) {
|
||||
requestedProfileName = System.getProperty(PROFILE_SYSTEM_PROPERTY);
|
||||
}
|
||||
|
||||
if (requestedProfileName != null && requestedProfileName.isBlank()) {
|
||||
requestedProfileName = null;
|
||||
}
|
||||
|
||||
return new CommandLineOptions(
|
||||
requestedProfileName == null ? null : requestedProfileName.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the parsed options for the lifetime of the process.
|
||||
*
|
||||
* @param options options to remember
|
||||
*/
|
||||
public static void remember(final CommandLineOptions options) {
|
||||
rememberedOptions = options == null ? new CommandLineOptions(null) : options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the options parsed at application startup.
|
||||
*
|
||||
* @return the remembered options, never null
|
||||
*/
|
||||
public static CommandLineOptions remembered() {
|
||||
return rememberedOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operator profile requested on the command line.
|
||||
*
|
||||
* @return the requested profile name, or null when none was given
|
||||
*/
|
||||
public String getRequestedProfileName() {
|
||||
return requestedProfileName;
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,9 @@ import javafx.stage.Screen;
|
||||
import kst4contest.logic.BandOpportunityResolver;
|
||||
import kst4contest.utils.ApplicationFileUtils;
|
||||
import kst4contest.view.map.StationMapBridge;
|
||||
import kst4contest.controller.ActiveOperatorProfile;
|
||||
import kst4contest.controller.OperatorProfileStore;
|
||||
import kst4contest.model.OperatorProfileSelection;
|
||||
import kst4contest.view.map.StationMapView;
|
||||
import kst4contest.view.map.OfflineDemImportService;
|
||||
import kst4contest.controller.WorkedGrossFieldCache;
|
||||
@@ -6227,6 +6230,66 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
return txMessageButtons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the operator profile this runtime works with.
|
||||
*
|
||||
* <p>Only executed on the very first launch. A profile switch sets the profile before
|
||||
* building the new runtime, so the resolution is skipped there.</p>
|
||||
*
|
||||
* <p>An installation with no or exactly one profile is resolved without asking
|
||||
* anything, which keeps the single operator startup exactly as it was.</p>
|
||||
*
|
||||
* @return true if the application may continue starting up
|
||||
*/
|
||||
private boolean resolveOperatorProfileIfRequired() {
|
||||
|
||||
if (ActiveOperatorProfile.isInitialized()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
OperatorProfileBootstrap bootstrap = new OperatorProfileBootstrap();
|
||||
OperatorProfileSelection resolvedProfile = bootstrap.resolveAtStartup(
|
||||
new OperatorProfileStore(),
|
||||
CommandLineOptions.remembered(),
|
||||
OperatorProfilePickerDialog::showAndSelect);
|
||||
|
||||
if (bootstrap.getStartupWarning() != null) {
|
||||
Alert startupWarning = new Alert(AlertType.WARNING);
|
||||
startupWarning.setTitle("Operator profile");
|
||||
startupWarning.setHeaderText("The requested operator profile was not found.");
|
||||
startupWarning.setContentText(bootstrap.getStartupWarning());
|
||||
startupWarning.showAndWait();
|
||||
}
|
||||
|
||||
if (resolvedProfile == null) {
|
||||
Platform.exit();
|
||||
System.exit(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
ActiveOperatorProfile.set(resolvedProfile);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the window title suffix naming the active operator profile.
|
||||
*
|
||||
* <p>Empty for the historic single profile installation, so nothing changes visually
|
||||
* for operators who never create a second profile.</p>
|
||||
*
|
||||
* @return the suffix to append to a window title, never null
|
||||
*/
|
||||
private String buildOperatorProfileTitleSuffix() {
|
||||
|
||||
OperatorProfileSelection activeProfile = ActiveOperatorProfile.get();
|
||||
|
||||
if (activeProfile == null || activeProfile.getProfile().isRootProfile()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return " - " + activeProfile.getProfile().getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
System.out.println("[Main.java, Info:] Stage is closing, killing all resources");
|
||||
@@ -6644,9 +6707,22 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
|
||||
Parameters applicationParameters = getParameters();
|
||||
|
||||
CommandLineOptions.remember(CommandLineOptions.parse(
|
||||
applicationParameters == null ? null : applicationParameters.getRaw()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) throws InterruptedException, IOException, URISyntaxException {
|
||||
|
||||
if (!resolveOperatorProfileIfRequired()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GuiUtils.applyApplicationIcon(primaryStage);
|
||||
|
||||
VBox pnl_inputAndSendButtons = new VBox(); //gets the sendtext field, send button and the timeline
|
||||
@@ -6681,8 +6757,15 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, STYLE_DEFAULTCSSDAY_RESOURCE, STYLE_DEFAULTCSSDAY_FILE);
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, STYLE_DEFAULTCSSEVENING_RESOURCE, STYLE_DEFAULTCSSEVENING_FILE);
|
||||
ChatMember ownChatMemberObject = new ChatMember();
|
||||
OperatorProfileSelection activeOperatorProfile = ActiveOperatorProfile.get();
|
||||
|
||||
chatcontroller = new ChatController(ownChatMemberObject, this); // instantiate the Chatcontroller with the user object
|
||||
// instantiate the Chatcontroller with the user object and the files of the active profile
|
||||
chatcontroller = new ChatController(
|
||||
ownChatMemberObject,
|
||||
this,
|
||||
activeOperatorProfile.getPreferencesRelativeFileName(),
|
||||
activeOperatorProfile.getWorkedDatabaseRelativeFileName(),
|
||||
activeOperatorProfile.isSeedWorkedDatabaseFromResource());
|
||||
layoutAutosave = new LayoutAutosave(chatcontroller.getChatPreferences());
|
||||
messageVariableResolver = new MessageVariableResolver(chatcontroller.getChatPreferences());
|
||||
chatcontroller.setStatusListener(this); //callback interface for updating Thread events in visual
|
||||
@@ -7200,7 +7283,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
txt_ownqrgSecondCategory.setFocusTraversable(false);
|
||||
txt_ownqrgSecondCategory.setTooltip(new Tooltip("Enter frequency for second chat-category here by hand! <fixme>"));
|
||||
|
||||
primaryStage.setTitle(chatcontroller.getChatPreferences().getChatState());
|
||||
primaryStage.setTitle(chatcontroller.getChatPreferences().getChatState() + buildOperatorProfileTitleSuffix());
|
||||
|
||||
timer_buildWindowTitle = new Timer();
|
||||
timer_buildWindowTitle.scheduleAtFixedRate(new TimerTask() {
|
||||
@@ -7255,7 +7338,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
chatcontroller.getChatPreferences().setChatState(chatState);
|
||||
}
|
||||
|
||||
primaryStage.setTitle(chatcontroller.getChatPreferences().getChatState());
|
||||
primaryStage.setTitle(chatcontroller.getChatPreferences().getChatState() + buildOperatorProfileTitleSuffix());
|
||||
|
||||
// System.out.println(chatcontroller.getChatPreferences().getChatState());
|
||||
});
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import kst4contest.controller.OperatorProfilePaths;
|
||||
import kst4contest.controller.OperatorProfileStore;
|
||||
import kst4contest.model.OperatorProfile;
|
||||
import kst4contest.model.OperatorProfileSelection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Decides which operator profile the application starts with.
|
||||
*
|
||||
* <p>The class contains no user interface code so the decision can be tested headless.
|
||||
* Asking the operator is delegated to an {@link OperatorProfileChoiceRequester}, and a
|
||||
* problem worth telling the operator about is reported through
|
||||
* {@link #getStartupWarning()} instead of being shown here.</p>
|
||||
*
|
||||
* <p>The most important property of this logic is what it does <em>not</em> do: an
|
||||
* installation with no or exactly one profile is resolved without asking anything and
|
||||
* without touching a single file, so a single operator start stays exactly as fast and
|
||||
* as quiet as it was before profiles existed.</p>
|
||||
*/
|
||||
public class OperatorProfileBootstrap {
|
||||
|
||||
private String startupWarning;
|
||||
|
||||
/**
|
||||
* Resolves the operator profile to start with.
|
||||
*
|
||||
* @param store registry to read the profiles from
|
||||
* @param commandLineOptions parsed command line options
|
||||
* @param choiceRequester requester used when the operator has to choose
|
||||
* @return the resolved selection, or null when the operator chose to quit
|
||||
*/
|
||||
public OperatorProfileSelection resolveAtStartup(final OperatorProfileStore store,
|
||||
final CommandLineOptions commandLineOptions,
|
||||
final OperatorProfileChoiceRequester choiceRequester) {
|
||||
|
||||
startupWarning = null;
|
||||
|
||||
List<OperatorProfile> availableProfiles = store.loadProfiles();
|
||||
String requestedProfileName = commandLineOptions == null
|
||||
? null
|
||||
: commandLineOptions.getRequestedProfileName();
|
||||
|
||||
if (requestedProfileName != null) {
|
||||
OperatorProfile requestedProfile = findProfile(availableProfiles, requestedProfileName);
|
||||
|
||||
if (requestedProfile != null) {
|
||||
return OperatorProfilePaths.resolve(requestedProfile);
|
||||
}
|
||||
|
||||
startupWarning = "The operator profile \"" + requestedProfileName
|
||||
+ "\" is unknown. KST4Contest continues with the normal profile selection.";
|
||||
}
|
||||
|
||||
if (availableProfiles.isEmpty()) {
|
||||
// No registry at all: the historic flat installation is the only profile.
|
||||
return OperatorProfilePaths.resolve(store.buildImplicitRootProfile());
|
||||
}
|
||||
|
||||
if (availableProfiles.size() == 1) {
|
||||
return OperatorProfilePaths.resolve(availableProfiles.get(0));
|
||||
}
|
||||
|
||||
String preselectedProfileId = store.loadLastUsedProfileId().orElse(null);
|
||||
Optional<OperatorProfile> chosenProfile =
|
||||
choiceRequester.requestProfileChoice(availableProfiles, preselectedProfileId);
|
||||
|
||||
return chosenProfile.map(OperatorProfilePaths::resolve).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a message that should be shown to the operator after startup.
|
||||
*
|
||||
* @return the warning text, or null when startup was unremarkable
|
||||
*/
|
||||
public String getStartupWarning() {
|
||||
return startupWarning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a profile by identifier or display name, ignoring case.
|
||||
*
|
||||
* @param availableProfiles profiles to search
|
||||
* @param requestedName identifier or display name entered by the operator
|
||||
* @return the matching profile, or null
|
||||
*/
|
||||
private static OperatorProfile findProfile(final List<OperatorProfile> availableProfiles,
|
||||
final String requestedName) {
|
||||
|
||||
for (OperatorProfile currentProfile : availableProfiles) {
|
||||
if (requestedName.equalsIgnoreCase(currentProfile.getProfileId())) {
|
||||
return currentProfile;
|
||||
}
|
||||
}
|
||||
|
||||
for (OperatorProfile currentProfile : availableProfiles) {
|
||||
if (requestedName.equalsIgnoreCase(currentProfile.getDisplayName())) {
|
||||
return currentProfile;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import kst4contest.model.OperatorProfile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Asks the operator which profile to start with.
|
||||
*
|
||||
* <p>The startup logic depends on this interface rather than on a dialog, so the
|
||||
* decision which profile to use can be tested without a JavaFX runtime.</p>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OperatorProfileChoiceRequester {
|
||||
|
||||
/**
|
||||
* Requests a profile choice.
|
||||
*
|
||||
* @param selectableProfiles profiles to choose from, never empty
|
||||
* @param preselectedProfileId identifier to preselect, may be null
|
||||
* @return the chosen profile, or empty when the operator wants to quit
|
||||
*/
|
||||
Optional<OperatorProfile> requestProfileChoice(List<OperatorProfile> selectableProfiles,
|
||||
String preselectedProfileId);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import kst4contest.model.OperatorProfile;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Asks the operator which profile to start with.
|
||||
*
|
||||
* <p>The dialog is shown only when more than one profile exists. It is intentionally
|
||||
* minimal, because it stands between the operator and a contest: the last used profile
|
||||
* is preselected, the list has the focus, and Enter or a double click start immediately.</p>
|
||||
*/
|
||||
public final class OperatorProfilePickerDialog {
|
||||
|
||||
private OperatorProfilePickerDialog() {
|
||||
// Utility class.
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the picker and waits for the operator's choice.
|
||||
*
|
||||
* @param selectableProfiles profiles to choose from
|
||||
* @param preselectedProfileId identifier of the profile to preselect, may be null
|
||||
* @return the chosen profile, or empty when the operator wants to quit
|
||||
*/
|
||||
public static Optional<OperatorProfile> showAndSelect(final List<OperatorProfile> selectableProfiles,
|
||||
final String preselectedProfileId) {
|
||||
|
||||
Stage dialogStage = new Stage();
|
||||
GuiUtils.applyApplicationIcon(dialogStage);
|
||||
dialogStage.initModality(Modality.APPLICATION_MODAL);
|
||||
dialogStage.setTitle("Select operator profile");
|
||||
|
||||
ListView<OperatorProfile> profileListView = new ListView<>();
|
||||
profileListView.getItems().addAll(selectableProfiles);
|
||||
profileListView.setCellFactory(listView -> new OperatorProfileListCell());
|
||||
VBox.setVgrow(profileListView, Priority.ALWAYS);
|
||||
|
||||
selectPreselectedProfile(profileListView, selectableProfiles, preselectedProfileId);
|
||||
|
||||
OperatorProfile[] chosenProfile = new OperatorProfile[1];
|
||||
|
||||
Button startButton = new Button("Start");
|
||||
startButton.setDefaultButton(true);
|
||||
startButton.setOnAction(event -> {
|
||||
chosenProfile[0] = profileListView.getSelectionModel().getSelectedItem();
|
||||
dialogStage.close();
|
||||
});
|
||||
|
||||
Button quitButton = new Button("Quit");
|
||||
quitButton.setCancelButton(true);
|
||||
quitButton.setOnAction(event -> {
|
||||
chosenProfile[0] = null;
|
||||
dialogStage.close();
|
||||
});
|
||||
|
||||
profileListView.setOnMouseClicked(event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
startButton.fire();
|
||||
}
|
||||
});
|
||||
|
||||
profileListView.setOnKeyPressed(event -> {
|
||||
if (event.getCode() == KeyCode.ENTER) {
|
||||
startButton.fire();
|
||||
}
|
||||
});
|
||||
|
||||
HBox buttonRow = new HBox(10, startButton, quitButton);
|
||||
buttonRow.setPadding(new Insets(10, 0, 0, 0));
|
||||
|
||||
VBox dialogContent = new VBox(8,
|
||||
new Label("More than one operator profile is configured."),
|
||||
profileListView,
|
||||
buttonRow);
|
||||
dialogContent.setPadding(new Insets(15));
|
||||
|
||||
dialogStage.setScene(new Scene(dialogContent, 380, 280));
|
||||
profileListView.requestFocus();
|
||||
dialogStage.showAndWait();
|
||||
|
||||
return Optional.ofNullable(chosenProfile[0]);
|
||||
}
|
||||
|
||||
private static void selectPreselectedProfile(final ListView<OperatorProfile> profileListView,
|
||||
final List<OperatorProfile> selectableProfiles,
|
||||
final String preselectedProfileId) {
|
||||
|
||||
int profileIndexToSelect = 0;
|
||||
|
||||
if (preselectedProfileId != null) {
|
||||
for (int profileIndex = 0; profileIndex < selectableProfiles.size(); profileIndex++) {
|
||||
if (preselectedProfileId.equalsIgnoreCase(
|
||||
selectableProfiles.get(profileIndex).getProfileId())) {
|
||||
profileIndexToSelect = profileIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
profileListView.getSelectionModel().select(profileIndexToSelect);
|
||||
profileListView.scrollTo(profileIndexToSelect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a profile with its name and the kind of worked data it uses.
|
||||
*/
|
||||
private static final class OperatorProfileListCell extends ListCell<OperatorProfile> {
|
||||
|
||||
@Override
|
||||
protected void updateItem(final OperatorProfile profile, final boolean empty) {
|
||||
|
||||
super.updateItem(profile, empty);
|
||||
|
||||
if (empty || profile == null) {
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
|
||||
String workedDataDescription = profile.isRootProfile() || profile.isSharedWorkedDatabase()
|
||||
? "shared station worked database"
|
||||
: "own worked database";
|
||||
|
||||
setText(profile.getDisplayName() + "\n" + workedDataDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class CommandLineOptionsTest {
|
||||
|
||||
@Test
|
||||
void profileArgumentIsAcceptedInBothSpellings() {
|
||||
assertEquals("OP2", CommandLineOptions.parse(List.of("--profile=OP2")).getRequestedProfileName());
|
||||
assertEquals("OP2", CommandLineOptions.parse(List.of("--profile", "OP2")).getRequestedProfileName());
|
||||
assertEquals("DM5M Contest",
|
||||
CommandLineOptions.parse(List.of("--profile", "DM5M Contest")).getRequestedProfileName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingOrEmptyProfileArgumentsAreTreatedAsAbsent() {
|
||||
assertNull(CommandLineOptions.parse(List.of()).getRequestedProfileName());
|
||||
assertNull(CommandLineOptions.parse(null).getRequestedProfileName());
|
||||
assertNull(CommandLineOptions.parse(List.of("--profile=")).getRequestedProfileName());
|
||||
assertNull(CommandLineOptions.parse(List.of("--profile")).getRequestedProfileName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unrelatedArgumentsAreIgnoredInsteadOfFailing() {
|
||||
assertEquals("OP2",
|
||||
CommandLineOptions.parse(List.of("--verbose", "--profile=OP2", "somefile.adi"))
|
||||
.getRequestedProfileName());
|
||||
assertNull(CommandLineOptions.parse(List.of("--verbose", "-x")).getRequestedProfileName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rememberedOptionsDefaultToEmptyInsteadOfNull() {
|
||||
CommandLineOptions.remember(null);
|
||||
assertNull(CommandLineOptions.remembered().getRequestedProfileName());
|
||||
|
||||
CommandLineOptions.remember(new CommandLineOptions("OP2"));
|
||||
assertEquals("OP2", CommandLineOptions.remembered().getRequestedProfileName());
|
||||
|
||||
CommandLineOptions.remember(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import kst4contest.controller.OperatorProfilePaths;
|
||||
import kst4contest.controller.OperatorProfileStore;
|
||||
import kst4contest.model.OperatorProfile;
|
||||
import kst4contest.model.OperatorProfileSelection;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class OperatorProfileBootstrapTest {
|
||||
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void installationWithoutRegistryStartsSilentlyOnTheHistoricLayout() {
|
||||
|
||||
AtomicInteger pickerInvocations = new AtomicInteger();
|
||||
OperatorProfileStore store = storeAt();
|
||||
|
||||
OperatorProfileSelection resolved = new OperatorProfileBootstrap().resolveAtStartup(
|
||||
store, new CommandLineOptions(null), countingPicker(pickerInvocations, null));
|
||||
|
||||
assertNotNull(resolved);
|
||||
assertEquals("preferences.xml", resolved.getPreferencesRelativeFileName());
|
||||
assertEquals("praktiKST.db", resolved.getWorkedDatabaseRelativeFileName());
|
||||
|
||||
// Nothing may be asked, and nothing may be written.
|
||||
assertEquals(0, pickerInvocations.get());
|
||||
assertTrue(store.loadProfiles().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleProfileStartsWithoutAskingAnything() {
|
||||
|
||||
AtomicInteger pickerInvocations = new AtomicInteger();
|
||||
OperatorProfileStore store = storeAt();
|
||||
store.saveProfiles(List.of(OperatorProfilePaths.buildRootProfile("Default")), "default");
|
||||
|
||||
OperatorProfileSelection resolved = new OperatorProfileBootstrap().resolveAtStartup(
|
||||
store, new CommandLineOptions(null), countingPicker(pickerInvocations, null));
|
||||
|
||||
assertNotNull(resolved);
|
||||
assertEquals(0, pickerInvocations.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void twoProfilesAskTheOperatorAndPreselectTheLastUsedOne() {
|
||||
|
||||
OperatorProfileStore store = storeAt();
|
||||
OperatorProfile secondProfile = new OperatorProfile("OP2", "DN9APW", false, false);
|
||||
store.saveProfiles(
|
||||
List.of(OperatorProfilePaths.buildRootProfile("Default"), secondProfile), "OP2");
|
||||
|
||||
AtomicInteger pickerInvocations = new AtomicInteger();
|
||||
String[] observedPreselection = new String[1];
|
||||
|
||||
OperatorProfileSelection resolved = new OperatorProfileBootstrap().resolveAtStartup(
|
||||
store,
|
||||
new CommandLineOptions(null),
|
||||
(profiles, preselectedProfileId) -> {
|
||||
pickerInvocations.incrementAndGet();
|
||||
observedPreselection[0] = preselectedProfileId;
|
||||
return Optional.of(profiles.get(1));
|
||||
});
|
||||
|
||||
assertEquals(1, pickerInvocations.get());
|
||||
assertEquals("OP2", observedPreselection[0]);
|
||||
assertNotNull(resolved);
|
||||
assertEquals("profiles/OP2/praktiKST.db", resolved.getWorkedDatabaseRelativeFileName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aValidProfileArgumentSkipsThePicker() {
|
||||
|
||||
OperatorProfileStore store = storeAt();
|
||||
store.saveProfiles(
|
||||
List.of(OperatorProfilePaths.buildRootProfile("Default"),
|
||||
new OperatorProfile("OP2", "DN9APW", false, false)),
|
||||
"default");
|
||||
|
||||
AtomicInteger pickerInvocations = new AtomicInteger();
|
||||
OperatorProfileBootstrap bootstrap = new OperatorProfileBootstrap();
|
||||
|
||||
OperatorProfileSelection byId = bootstrap.resolveAtStartup(
|
||||
store, new CommandLineOptions("op2"), countingPicker(pickerInvocations, null));
|
||||
|
||||
assertEquals(0, pickerInvocations.get());
|
||||
assertEquals("profiles/OP2/preferences.xml", byId.getPreferencesRelativeFileName());
|
||||
assertNull(bootstrap.getStartupWarning());
|
||||
|
||||
OperatorProfileSelection byDisplayName = bootstrap.resolveAtStartup(
|
||||
store, new CommandLineOptions("DN9APW"), countingPicker(pickerInvocations, null));
|
||||
|
||||
assertEquals(0, pickerInvocations.get());
|
||||
assertEquals("profiles/OP2/preferences.xml", byDisplayName.getPreferencesRelativeFileName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void anUnknownProfileArgumentWarnsAndFallsBackToTheNormalSelection() {
|
||||
|
||||
OperatorProfileStore store = storeAt();
|
||||
store.saveProfiles(
|
||||
List.of(OperatorProfilePaths.buildRootProfile("Default"),
|
||||
new OperatorProfile("OP2", "DN9APW", false, false)),
|
||||
"default");
|
||||
|
||||
AtomicInteger pickerInvocations = new AtomicInteger();
|
||||
OperatorProfileBootstrap bootstrap = new OperatorProfileBootstrap();
|
||||
|
||||
OperatorProfileSelection resolved = bootstrap.resolveAtStartup(
|
||||
store, new CommandLineOptions("NOPE"), countingPicker(pickerInvocations, 0));
|
||||
|
||||
assertEquals(1, pickerInvocations.get());
|
||||
assertNotNull(resolved);
|
||||
assertNotNull(bootstrap.getStartupWarning());
|
||||
assertTrue(bootstrap.getStartupWarning().contains("NOPE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void quittingInThePickerYieldsNoSelection() {
|
||||
|
||||
OperatorProfileStore store = storeAt();
|
||||
store.saveProfiles(
|
||||
List.of(OperatorProfilePaths.buildRootProfile("Default"),
|
||||
new OperatorProfile("OP2", "DN9APW", false, false)),
|
||||
"default");
|
||||
|
||||
OperatorProfileSelection resolved = new OperatorProfileBootstrap().resolveAtStartup(
|
||||
store, new CommandLineOptions(null), (profiles, preselected) -> Optional.empty());
|
||||
|
||||
assertNull(resolved);
|
||||
}
|
||||
|
||||
private OperatorProfileChoiceRequester countingPicker(final AtomicInteger invocationCounter,
|
||||
final Integer profileIndexToChoose) {
|
||||
return (profiles, preselectedProfileId) -> {
|
||||
invocationCounter.incrementAndGet();
|
||||
|
||||
if (profileIndexToChoose == null) {
|
||||
return Optional.of(profiles.get(0));
|
||||
}
|
||||
|
||||
return Optional.of(profiles.get(profileIndexToChoose));
|
||||
};
|
||||
}
|
||||
|
||||
private OperatorProfileStore storeAt() {
|
||||
return new OperatorProfileStore(temporaryDirectory.resolve("profiles.xml").toString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user