mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 13:53:01 +02:00
* docs: add the test-coverage design spec (#62) Records the state found for issue #62 (JUnit classes under src/main that never run, test libraries in the shipped app image, dead 2023/2024 scratch tests, no coverage figure), the decisions taken (delete, move to src/test, JaCoCo report-only without a threshold, tests before packaging), what is deliberately left to separate issues, and the definition of done. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: move the JUnit classes out of src/main and drop the dead 2023 tests (#62) Gradle's test task scans src/test only, so the six JUnit classes under src/main compiled on every build and were never executed. They move to src/test of the module that owns the class under test, keeping their package names: On4KstProtocolTest, On4KstSocketThreadTest and ChatMemberDirectionOpportunityTest within core; Utils4KSTFrequencyTest, FrequencyTextParserTest and ChatPreferencesStationMapVisibilityTest from app-desktop into core, next to the sibling tests that already live there, because they test core classes only. All 26 of them run and pass. The fully commented-out MessageBusManagementThreadFrequencyContextTest is deleted. Eight files under core/src/test/java/kst4contest/test from 2023/2024 go as well: they carry no assertion, five of them annotate a static main() with @Test, which Jupiter never runs, and some write the real preferences file, send real UDP broadcasts or open the real user database when started by hand. The five main() scratch programs under src/main (ReadUDPByWintestThreadTest, PatternMatcherTest, mapTest, RegexTester, locatorUtils/TestLocatorUtils) and MockKstServer, which only an unused import in ChatController still referenced, are removed too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * build: make JUnit and Mockito test-only dependencies (#62) With no test class left under src/main, the implementation declarations of junit-jupiter-api and mockito-core in core and app-desktop lose their only justification. They put junit-jupiter-api, junit-platform-commons, opentest4j, mockito-core, byte-buddy, byte-buddy-agent and objenesis on the runtime classpath and into lib/app of every app image, Flatpak and installer. The duplicate catalog entries junit-jupiter-api, mockito-core-compile and the mockitoCompile version go with them. Verified: `:app-desktop:dependencies --configuration runtimeClasspath` no longer lists any of the seven artifacts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: state that test failures fail the build and tests live under src/test (#62) AGENTS.md and docs/PROJECT_CONTEXT.md still said the build configuration had allowed test failures to pass. That was true of the Maven build; under Gradle a test failure fails the build, and only PMD and SpotBugs run with ignoreFailures. Both documents now say so, note that test classes live under src/test only and that JUnit and Mockito are test-only dependencies, and keep the rule to read the test-result XML rather than the exit code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * build: add a JaCoCo coverage report (#62) The jacoco plugin is applied to every module from the root build with the tool version held in the catalog. jacocoTestReport now depends on test and writes XML and HTML per module; a root task jacocoAggregatedReport merges core and app-desktop into build/reports/jacoco/aggregated/ (XML plus HTML). i18n-generator is excluded: it is a build tool and its coverage says nothing about the shipped application. Kotlin classes are counted, because the Kotlin plugin adds its output to the main source set the report reads. Report only, on purpose. There is no jacocoTestCoverageVerification and no threshold: a minimum set at today's number would trip on the next unrelated change, and one set at a target would fail every build until tests exist for the number's sake. The figure is published so it can be read when deciding what to test next; the reasoning is in docs/superpowers/specs/2026-10-11-test-coverage-design.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: run the tests before packaging and publish coverage (#62) The nightly and tagged-release workflows built seven package formats from commits nobody had tested on the way: pr-compile-check.yml runs the suite for pull requests only, and main can be pushed to directly. Both workflows now carry a copy of that test job, including the libgl1 step and the XML-based summary, and every packaging job declares `needs: test`. Copied rather than factored into a reusable workflow on purpose: the comments in the job explain decisions the packaging workflows should carry with them, and three copies read better than one indirection. build-docs-pdf runs pandoc only and is left alone; publish-flatpak-repo and release-tag are gated through the jobs they already need. The PR test job additionally runs jacocoTestReport and jacocoAggregatedReport once the tests have run (not `if: always()`: a report from an incomplete run would mislead), prints a line/branch coverage table per module and aggregated into the job summary from the JaCoCo XML, and uploads the aggregated HTML report as the coverage-report artifact for 14 days whenever it exists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: describe the coverage report in the project context (#62) A short paragraph on JaCoCo in docs/PROJECT_CONTEXT.md: report only and why, where the per-module and aggregated reports land, which Gradle tasks write them, that i18n-generator is excluded, how the PR job summary and the coverage-report artifact show the figure, and that the nightly and tagged-release workflows run the tests before any packaging job. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: round-trip every ChatPreferences field through the XML file (#62) Drive all public single-argument setters by reflection to non-default values, write preferences.xml, read it back and compare every getter. Fields that are not persisted on purpose are listed with their evidence; two real reader/writer gaps (second-category locator, sniffer words) are documented with a BUG marker instead of being fixed here. Two further tests load the bundled template and a legacy file without configVersion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: cover the DBController schema migration from older user_version values (#62) Open the seeded template database and hand-built release 1.1 and 1.3 schema fixtures with a DBController and verify that PRAGMA user_version reaches the current level, every column of the current CREATE TABLE statements exists, legacy callsign keys are merged to base callsigns and existing rows and flags survive. A further test proves that reopening an up-to-date database changes neither schema nor rows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: drop the committed JVM crash report and ignore future ones (#62) hs_err_pid26344.log in the repository root was a HotSpot crash dump that had been committed by accident. It carries no information about the project and must not travel with the sources. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: drop the dead QSO sniffer word list that corrupted the callsign list on load (#103) ChatPreferences carried a second sniffer list, lstNotify_QSOSniffer_sniffedWordsList, that no UI, no manual and no consumer outside ChatPreferences ever touched. It was still written to preferences.xml as <snifferWords><w>, and the matching reader block was a copy of the <snifferCallSigns> block: it cleared the word list and then added every word, normalised as a callsign, to lstNotify_QSOSniffer_sniffedCallSignList. Any entry that ever reached the word list therefore leaked into QSO monitoring on every load. Remove the field, its accessors, the write block and the read block. A <snifferWords> element in an older preferences file is now ignored like any unknown element; the callsign list and its persistence are unchanged. ChatPreferencesSnifferListTest loads a file that carries both lists and asserts that only the <snifferCallSigns> entry is monitored. The round-trip test loses its exclusion entry for the removed setter. Fixes #103 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: remove the second-category locator that could never be set (#104) ChatPreferences declared stn_loginLocatorSecondCat with the compiled-in default "jn49fk". Nothing wrote it to or read it from preferences.xml, no settings tab exposed it and no caller ever invoked the setter, so the field always held its default. On4KstConnectionManager.configureMainChat nevertheless compared that default with the station's locator and, with the second chat enabled, raised the warning "ON4KST uses one locator per TCP session. The second-chat locator ... is ignored" whenever the station's locator was not jn49fk, which is practically every installation. ON4KST uses one locator per TCP session by design, so a second-category locator can never become meaningful. Remove the field, its accessors, the comparison and the warning; the rest of configureMainChat is unchanged. The round-trip test loses its exclusion entry for the removed setter. No automated test covers configureMainChat: it is private, works on the manager's private Session and needs a mocked ChatController plus a live socket handshake, so a harness would far exceed the scope of this fix. The compile of core and app-desktop and the shorter exclusion list are the evidence that nothing references the member. Fixes #104 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4a90ae7d30
commit
5491cbcf8b
42 files changed
+1205
-2025
No files matched your search
@@ -22,7 +22,6 @@ import kst4contest.locatorUtils.DirectionUtils;
|
||||
import kst4contest.logic.BandOpportunityResolver;
|
||||
import kst4contest.logic.PriorityCalculator;
|
||||
import kst4contest.model.*;
|
||||
import kst4contest.test.MockKstServer;
|
||||
import kst4contest.utils.ApplicationFileUtils;
|
||||
import kst4contest.utils.PlayAudioUtils;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
//package kst4contest.controller;
|
||||
//
|
||||
//import kst4contest.model.Band;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.junit.jupiter.params.ParameterizedTest;
|
||||
//import org.junit.jupiter.params.provider.ValueSource;
|
||||
//
|
||||
//import java.util.regex.Matcher;
|
||||
//import java.util.regex.Pattern;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
//import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
//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 MessageBusManagementThreadFrequencyContextTest {
|
||||
//
|
||||
// private static final Pattern THREE_DIGIT_VALUE =
|
||||
// Pattern.compile("\\b\\d{3}\\b");
|
||||
//
|
||||
// @ParameterizedTest
|
||||
// @ValueSource(strings = {
|
||||
// "qrg 210",
|
||||
// "QRG: 210",
|
||||
// "freq is 210",
|
||||
// "frequency = 210",
|
||||
// "on 210",
|
||||
// "210 MHz",
|
||||
// "210 qrg"
|
||||
// })
|
||||
// void acceptsBareThreeDigitValueWithFrequencyContext(String messageText) {
|
||||
// Matcher matcher = findThreeDigitValue(messageText);
|
||||
//
|
||||
// assertTrue(
|
||||
// MessageBusManagementThread
|
||||
// .hasExplicitBareFrequencyContext(
|
||||
// messageText,
|
||||
// matcher.start(),
|
||||
// matcher.end()
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// @ParameterizedTest
|
||||
// @ValueSource(strings = {
|
||||
// "599",
|
||||
// "144",
|
||||
// "serial 210",
|
||||
// "score 210",
|
||||
// "worked 210 stations"
|
||||
// })
|
||||
// void rejectsBareThreeDigitValueWithoutFrequencyContext(String messageText) {
|
||||
// Matcher matcher = findThreeDigitValue(messageText);
|
||||
//
|
||||
// assertFalse(
|
||||
// MessageBusManagementThread
|
||||
// .hasExplicitBareFrequencyContext(
|
||||
// messageText,
|
||||
// matcher.start(),
|
||||
// matcher.end()
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void resolvesOnlySupportedFallbackPrefixes() {
|
||||
// assertEquals(Band.B_144, Band.fromPrefix("144"));
|
||||
// assertEquals(Band.B_432, Band.fromPrefix(" 432 "));
|
||||
// assertEquals(Band.B_10G, Band.fromPrefix("10368"));
|
||||
// assertNull(Band.fromPrefix("999"));
|
||||
// assertNull(Band.fromPrefix(null));
|
||||
// }
|
||||
//
|
||||
// private Matcher findThreeDigitValue(String messageText) {
|
||||
// Matcher matcher = THREE_DIGIT_VALUE.matcher(messageText);
|
||||
// assertTrue(matcher.find(), "Test message must contain a three-digit value");
|
||||
// assertNotNull(matcher.group());
|
||||
// return matcher;
|
||||
// }
|
||||
//}
|
||||
@@ -509,16 +509,6 @@ final class On4KstConnectionManager {
|
||||
category, preferences.getStn_loginNameMainCat()));
|
||||
}
|
||||
sendControl(session, On4KstProtocol.back(category));
|
||||
|
||||
String secondLocator = preferences.getStn_loginLocatorSecondCat();
|
||||
String mainLocator = preferences.getStn_loginLocatorMainCat();
|
||||
if (preferences.isLoginToSecondChatEnabled()
|
||||
&& secondLocator != null && !secondLocator.isBlank()
|
||||
&& !secondLocator.equalsIgnoreCase(mainLocator)) {
|
||||
controller.onOn4KstConnectionWarning(
|
||||
"ON4KST uses one locator per TCP session. The second-chat locator '"
|
||||
+ secondLocator + "' is ignored; using '" + mainLocator + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void configureSecondChat(Session session) {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
public class ReadUDPByWintestThreadTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ChatController ctrl1 = new ChatController();
|
||||
|
||||
ChatPreferences prefs = new ChatPreferences();
|
||||
ctrl1.setChatPreferences(prefs);
|
||||
|
||||
// ReadUDPByWintestThread test = new ReadUDPByWintestThread(ctrl1);
|
||||
|
||||
|
||||
// test.run();
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
public class RegexTester {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
|
||||
String temp;
|
||||
|
||||
temp = "DL1KDA JO30EP Alex\r\n"
|
||||
+ "DL3LST JO61FI Ray(Rainer)\r\n"
|
||||
+ "DL4ZAQ JN49MP Peter 2m&70cm&6m\r\n"
|
||||
+ "DL9LBH JN59ID Hans 2/70/23\r\n"
|
||||
+ "DO5AMF JN49FK Marc\r\n"
|
||||
+ "F5ICN JN03BF Alex QRV 2/70/23\r\n"
|
||||
+ "F6GRA JN04DB Carol 2m\r\n"
|
||||
+ "F6HTJ JN12KQ Michel\r\n"
|
||||
+ "G0BIX JO01GI Terry\r\n"
|
||||
+ "G0HOF IO92UL Ken 11el + 400 W\r\n"
|
||||
+ "G0LBK JO03BD David\r\n"
|
||||
+ "G0MBL JO01QH Andrew 2m\r\n"
|
||||
+ "G1SDX IO80FL Grant\r\n"
|
||||
+ "G3MXH JO02LF Terry\r\n"
|
||||
+ "G3OVH IO92JP Tony\r\n"
|
||||
+ "G3VCA IO93MG bob\r\n"
|
||||
+ "G4AEP IO91MB bill\r\n"
|
||||
+ "G4DHF IO92UU David\r\n"
|
||||
+ "G4FUF JO01GN Keith\r\n"
|
||||
+ "G4TRA IO81WN Steve\r\n"
|
||||
+ "G6HKS IO92OB Richard\r\n"
|
||||
+ "G8SEI IO92FO Jeff 6/4/2/70\r\n"
|
||||
+ "GM0EWX IO67UL Calum\r\n"
|
||||
+ "GW8IZR IO73TI Paul\r\n"
|
||||
+ "IV3GTH JN65RU Gigi\r\n"
|
||||
+ "IZ2XZM JN45KH Nick\r\n"
|
||||
+ "LY1BWB KO24PR VU Club 2/70\r\n"
|
||||
+ "LY2EN KO24PQ QRT\r\n"
|
||||
+ "OK1FPR JO80CE Milos\r\n"
|
||||
+ "OV3T JO46CM Thomas\r\n"
|
||||
+ "OY4TN IP62NB Trygvi 11EL/350W\r\n"
|
||||
+ "OZ2M JO65FR Bo PNGonVHF\r\n"
|
||||
+ "OZ3Z JO45UN Anders @432.228\r\n"
|
||||
+ "OZ7UV JO65DH Svend\r\n"
|
||||
+ "PA3DOL JO22MT Sjoerd\r\n"
|
||||
+ "PA3PCV JO20VV Marcel 2m\r\n"
|
||||
+ "PA9R JO22JK Rob\r\n"
|
||||
+ "S52FO JN76EF Janez\r\n"
|
||||
+ "S53RM JN76HD Sine\r\n"
|
||||
+ "SM2CEW KP15CR Peter\r\n"
|
||||
+ "SP6VGJ JO81HU Jacek";
|
||||
|
||||
|
||||
Pattern pattern = Pattern.compile("([a-zA-Z0-9]{2}/{1})?([a-zA-Z0-9]{1,3}[0-9][a-zA-Z0-9]{0,3}[a-zA-Z]{0,3})(/p)? [a-zA-Z]{2}[0-9]{2}[a-zA-Z]{2} [ -~]{1,20}");
|
||||
Matcher matcher = pattern.matcher(temp);
|
||||
/**
|
||||
* "([a-zA-Z0-9]{1,2}\/)?[a-zA-Z0-9]{1,3}[0-9][a-zA-Z0-9]{0,3}[a-zA-Z](\/(p|m))?( )[a-zA-Z]{2}[0-9]{2}[a-zA-Z]{2}[ -~]{0,30}"gm
|
||||
* Thats a line of the show users list
|
||||
*/
|
||||
while (matcher.find()) {
|
||||
// System.out.println("Chatmember detected: "+ matcher.group() + " " + matcher.start());
|
||||
|
||||
ChatMember member = new ChatMember();
|
||||
String matchedString = matcher.group();
|
||||
|
||||
String[] splittedUserString;
|
||||
splittedUserString = matchedString.split(" ");
|
||||
|
||||
member.setCallSign(splittedUserString[0]);
|
||||
member.setQra(splittedUserString[1]);
|
||||
|
||||
String stringAggregation = "";
|
||||
for (int i = 2; i < splittedUserString.length; i++) {
|
||||
stringAggregation += splittedUserString[i] + " ";
|
||||
}
|
||||
member.setName(stringAggregation);
|
||||
|
||||
System.out.println("Call: " + member.getCallSign() + ", QRA: " + member.getQra() + ", Name: " + member.getName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package kst4contest.locatorUtils;
|
||||
|
||||
public class TestLocatorUtils {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
|
||||
// isInAngle(myLocation, location1, location2);
|
||||
System.out.println(isInAngleAndRange("JN49FL", "jo43xm", "jo30sa", 900, 50));
|
||||
System.out.println(isInAngleAndRange("JN49FL", "jo51ij", "jn39oc", 900, 50));
|
||||
System.out.println(isInAngleAndRange("JN49FL", "jn39oc", "jo51ij", 1100, 50));
|
||||
}
|
||||
|
||||
public static boolean isInAngleAndRange(String myLocator, String locatorOfSkedSender, String locatorOfSekdReceiver, double maxRangeKm, double hisAntennaBeamWidth) {
|
||||
|
||||
Location myLocation = new Location(myLocator);
|
||||
Location skedSenderLocation = new Location(locatorOfSkedSender);
|
||||
Location skedReceiverLocation = new Location(locatorOfSekdReceiver);
|
||||
|
||||
double distanceFromMeToLocSender = new Location(myLocator).getDistanceKm(new Location(locatorOfSkedSender));
|
||||
|
||||
// Check if distance exceeds my setted maximum range
|
||||
if (distanceFromMeToLocSender > maxRangeKm) {
|
||||
System.out.println("too far, " + distanceFromMeToLocSender + " km");
|
||||
return false;
|
||||
}
|
||||
|
||||
//check bearing of sender to receiver
|
||||
|
||||
double bearingOfSekdSenderToSkedReceiver = skedSenderLocation.getBearing(skedReceiverLocation);
|
||||
System.out.println("skedTX -> skedRX deg: " + bearingOfSekdSenderToSkedReceiver);
|
||||
|
||||
double bearingOfSekdSenderToMe = skedSenderLocation.getBearing(myLocation);
|
||||
System.out.println("skedTX -> me deg: " + bearingOfSekdSenderToMe);
|
||||
|
||||
if (DirectionUtils.isAngleInRange(bearingOfSekdSenderToSkedReceiver, bearingOfSekdSenderToMe, hisAntennaBeamWidth)) {
|
||||
//may I should get "/2" because of 50% of the 3dB opening angle if txer is directed to sender exactly
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -218,7 +218,6 @@ public class ChatPreferences {
|
||||
String stn_loginNameMainCat = "KST4Contest";
|
||||
String stn_loginNameSecondCat = "KST4ContestSHF";
|
||||
String stn_loginLocatorMainCat = "jn49fk";
|
||||
String stn_loginLocatorSecondCat = "jn49fk";
|
||||
|
||||
double stn_antennaBeamWidthDeg = 50;
|
||||
double stn_maxQRBDefault = 900;
|
||||
@@ -323,7 +322,6 @@ public class ChatPreferences {
|
||||
lstNotify_QSOSniffer_sniffedCallSignList =
|
||||
new SimpleRoster<>();
|
||||
// SimpleRoster<String> lstNotify_QSOSniffer_sniffedCallSignList = new SimpleRoster<>();
|
||||
SimpleRoster<String> lstNotify_QSOSniffer_sniffedWordsList = new SimpleRoster<>();
|
||||
SimpleRoster<String> lstNotify_QSOSniffer_sniffedPrefixLocList = new SimpleRoster<>();
|
||||
|
||||
// Scoring / interaction metrics
|
||||
@@ -554,15 +552,6 @@ public class ChatPreferences {
|
||||
return lstNotify_QSOSniffer_sniffedCallSignList;
|
||||
}
|
||||
|
||||
public SimpleRoster<String> getLstNotify_QSOSniffer_sniffedWordsList() {
|
||||
return lstNotify_QSOSniffer_sniffedWordsList;
|
||||
}
|
||||
|
||||
public void setLstNotify_QSOSniffer_sniffedWordsList(java.util.Collection<String> lstNotify_QSOSniffer_sniffedWordsList) {
|
||||
// Fill the existing roster: replacing the field would orphan its listeners.
|
||||
this.lstNotify_QSOSniffer_sniffedWordsList.setAll(lstNotify_QSOSniffer_sniffedWordsList);
|
||||
}
|
||||
|
||||
public SimpleRoster<String> getLstNotify_QSOSniffer_sniffedPrefixLocList() {
|
||||
return lstNotify_QSOSniffer_sniffedPrefixLocList;
|
||||
}
|
||||
@@ -660,14 +649,6 @@ public class ChatPreferences {
|
||||
this.logsynch_wintestUsePassQrg = logsynch_wintestUsePassQrg;
|
||||
}
|
||||
|
||||
public String getStn_loginLocatorSecondCat() {
|
||||
return stn_loginLocatorSecondCat;
|
||||
}
|
||||
|
||||
public void setStn_loginLocatorSecondCat(String stn_loginLocatorSecondCat) {
|
||||
this.stn_loginLocatorSecondCat = stn_loginLocatorSecondCat;
|
||||
}
|
||||
|
||||
public boolean isBcn_beaconsEnabledSecondCat() {
|
||||
return bcn_beaconsEnabledSecondCat;
|
||||
}
|
||||
@@ -1962,15 +1943,6 @@ public class ChatPreferences {
|
||||
/**
|
||||
* QSO Sniffer Lists
|
||||
*/
|
||||
Element snifferWords = doc.createElement("snifferWords");
|
||||
rootElement.appendChild(snifferWords);
|
||||
|
||||
for (String word : lstNotify_QSOSniffer_sniffedWordsList.snapshot()) {
|
||||
Element temp = doc.createElement("w");
|
||||
temp.setTextContent(word);
|
||||
snifferWords.appendChild(temp);
|
||||
}
|
||||
|
||||
Element snifferCallSigns =
|
||||
doc.createElement("snifferCallSigns");
|
||||
rootElement.appendChild(snifferCallSigns);
|
||||
@@ -2935,47 +2907,6 @@ public class ChatPreferences {
|
||||
}
|
||||
}
|
||||
|
||||
list = doc.getElementsByTagName("snifferWords");
|
||||
if (list != null && list.getLength() != 0) {
|
||||
// reset to avoid duplicates when reloading
|
||||
lstNotify_QSOSniffer_sniffedWordsList.clear();
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
Node node = list.item(temp);
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
Element element = (Element) node;
|
||||
NodeList children = element.getChildNodes();
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node child = children.item(i);
|
||||
if (child.getNodeType() == Node.ELEMENT_NODE) {
|
||||
String callSign = child.getTextContent();
|
||||
|
||||
if (callSign != null
|
||||
&& !callSign.isBlank()) {
|
||||
|
||||
/*
|
||||
* Older preference files may contain complete KST callsigns
|
||||
* such as DN9APW-2. QSO monitoring is stored by base callsign,
|
||||
* so one entry covers every suffix of the same station.
|
||||
*/
|
||||
String normalizedCallSign =
|
||||
ChatMember.normalizeCallSignToBaseCallSign(
|
||||
callSign
|
||||
);
|
||||
|
||||
if (normalizedCallSign != null
|
||||
&& !normalizedCallSign.isBlank()
|
||||
&& !lstNotify_QSOSniffer_sniffedCallSignList.snapshot()
|
||||
.contains(normalizedCallSign)) {
|
||||
lstNotify_QSOSniffer_sniffedCallSignList
|
||||
.add(normalizedCallSign);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list = doc.getElementsByTagName("snifferPrefixes");
|
||||
if (list != null && list.getLength() != 0) {
|
||||
lstNotify_QSOSniffer_sniffedPrefixLocList.clear();
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public class MockKstServer {
|
||||
|
||||
private static final int PORT = 23001;
|
||||
private static final String CHAT_ID = "2"; // 2 = 144/432 MHz
|
||||
|
||||
// Thread-sichere Liste aller verbundenen Clients (OutputStreams)
|
||||
private final List<PrintWriter> clients = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Permanente User (Ihre Test-Callsigns)
|
||||
private final Map<String, User> onlineUsers = new HashMap<>();
|
||||
// Historien müssen synchronisiert werden
|
||||
private final List<String> historyChat = Collections.synchronizedList(new ArrayList<>());
|
||||
private final List<String> historyDx = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
private boolean running = false;
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
public MockKstServer() {
|
||||
// Initiale Permanente User
|
||||
addUser("DK5EW", "Erwin", "JN47NX");
|
||||
addUser("DL1TEST", "TestOp", "JO50XX");
|
||||
addUser("ON4KST", "Alain", "JO20HI");
|
||||
addUser("PA9R-2", "2", "JO20HI");
|
||||
addUser("PA9R-70", "70", "JO20HI");
|
||||
addUser("PA9R", "general", "JO20HI");
|
||||
}
|
||||
|
||||
// Startet den Server im Hintergrund (Non-Blocking)
|
||||
public void start() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
serverSocket = new ServerSocket(PORT);
|
||||
System.out.println("[Server] ON4KST Simulation gestartet auf Port " + PORT);
|
||||
|
||||
// Startet den Simulator für Zufallstraffic
|
||||
new Thread(this::simulationLoop).start();
|
||||
|
||||
while (running) {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
System.out.println("[Server] Neuer Client verbunden: " + clientSocket.getInetAddress());
|
||||
new Thread(new ClientHandler(clientSocket)).start();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (running) e.printStackTrace();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
try {
|
||||
if (serverSocket != null) serverSocket.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void addUser(String call, String name, String loc) {
|
||||
onlineUsers.put(call, new User(call, name, loc));
|
||||
}
|
||||
|
||||
private void removeUser(String call) {
|
||||
onlineUsers.remove(call);
|
||||
}
|
||||
|
||||
// Sendet Nachricht an ALLE verbundenen Clients (inkl. Sender)
|
||||
private void broadcast(String message) {
|
||||
if (!message.endsWith("\r\n")) message += "\r\n";
|
||||
String finalMsg = message;
|
||||
|
||||
for (PrintWriter writer : clients) {
|
||||
try {
|
||||
writer.print(finalMsg);
|
||||
writer.flush(); // WICHTIG: Sofort senden!
|
||||
} catch (Exception e) {
|
||||
// Client wohl weg, wird beim nächsten Schreibversuch oder im Handler entfernt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Innere Logik: Client Handler ---
|
||||
private class ClientHandler implements Runnable {
|
||||
private Socket socket;
|
||||
private PrintWriter out;
|
||||
private BufferedReader in;
|
||||
private String myCall = "MYCLIENT"; // Default, wird bei LOGIN überschrieben
|
||||
|
||||
public ClientHandler(Socket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// ISO-8859-1 ist Standard für KST/Telnet Cluster
|
||||
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ISO-8859-1"));
|
||||
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"), true);
|
||||
|
||||
clients.add(out);
|
||||
|
||||
String line;
|
||||
boolean loginComplete = false;
|
||||
|
||||
while ((line = in.readLine()) != null) {
|
||||
// System.out.println("[RECV] " + line); // Debugging aktivieren falls nötig
|
||||
String[] parts = line.split("\\|");
|
||||
String cmd = parts[0];
|
||||
|
||||
if (cmd.equals("LOGIN") || cmd.equals("LOGINC")) {
|
||||
// Protokoll: LOGIN|callsign|password|... [cite: 21]
|
||||
if (parts.length > 1) myCall = parts[1];
|
||||
|
||||
// 1. Login Bestätigung
|
||||
// Format: LOGSTAT|100|chat id|client software version|session key|config|dx option|
|
||||
send("LOGSTAT|100|" + CHAT_ID + "|JavaSim|KEY123|Config|3|");
|
||||
|
||||
// Bei LOGIN senden wir die Daten sofort
|
||||
// Bei LOGINC warten wir eigentlich auf SDONE, senden hier aber vereinfacht direkt
|
||||
if (cmd.equals("LOGIN")) {
|
||||
sendInitialData();
|
||||
loginComplete = true;
|
||||
}
|
||||
}
|
||||
else if (cmd.equals("SDONE")) {
|
||||
// Abschluss der Settings (bei LOGINC) [cite: 34]
|
||||
sendInitialData();
|
||||
loginComplete = true;
|
||||
}
|
||||
else if (cmd.equals("MSG")) {
|
||||
// MSG|chat id|destination|command|0| [cite: 42]
|
||||
if (parts.length >= 4) {
|
||||
String text = parts[3];
|
||||
// Nachricht sofort als CH Frame an alle verteilen (Echo)
|
||||
handleChatMessage(myCall, "Me", text);
|
||||
}
|
||||
}
|
||||
else if (cmd.equals("CK")) {
|
||||
// Keepalive [cite: 20]
|
||||
// Server muss nicht zwingend antworten, aber Connection bleibt offen
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// System.out.println("Client getrennt");
|
||||
} finally {
|
||||
clients.remove(out);
|
||||
try { socket.close(); } catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
private void send(String msg) {
|
||||
if (!msg.endsWith("\r\n")) msg += "\r\n";
|
||||
out.print(msg);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private void sendInitialData() {
|
||||
// 1. User Liste UA0 [cite: 14]
|
||||
for (User u : onlineUsers.values()) {
|
||||
send("UA0|" + CHAT_ID + "|" + u.call + "|" + u.name + "|" + u.loc + "|0|");
|
||||
}
|
||||
// 2. Chat History CR [cite: 7]
|
||||
synchronized(historyChat) {
|
||||
for (String h : historyChat) send(h);
|
||||
}
|
||||
// 3. DX History DL [cite: 10]
|
||||
synchronized(historyDx) {
|
||||
for (String d : historyDx) send(d);
|
||||
}
|
||||
// 4. Ende User Liste UE [cite: 15]
|
||||
send("UE|" + CHAT_ID + "|" + onlineUsers.size() + "|");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hilfsmethoden für Traffic ---
|
||||
|
||||
private void handleChatMessage(String call, String name, String text) {
|
||||
// CH|chat id|date|callsign|firstname|destination|msg|highlight|
|
||||
String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String frame = String.format("CH|%s|%s|%s|%s|0|%s|0|", CHAT_ID, date, call, name, text);
|
||||
|
||||
synchronized(historyChat) {
|
||||
historyChat.add(frame);
|
||||
if (historyChat.size() > 50) historyChat.remove(0);
|
||||
}
|
||||
broadcast(frame);
|
||||
}
|
||||
|
||||
private void handleDxSpot(String spotter, String dx, String freq) {
|
||||
// DL|Unix time|dx utc|spotter|qrg|dx|info|spotter locator|dx locator| [cite: 10]
|
||||
long unixTime = System.currentTimeMillis() / 1000;
|
||||
String utc = new SimpleDateFormat("HHmm").format(new Date());
|
||||
// Simple Dummy Locators
|
||||
String frame = String.format("DL|%d|%s|%s|%s|%s|Simulated|JO00|JO99|",
|
||||
unixTime, utc, spotter, freq, dx);
|
||||
|
||||
synchronized(historyDx) {
|
||||
historyDx.add(frame);
|
||||
if (historyDx.size() > 20) historyDx.remove(0);
|
||||
}
|
||||
broadcast(frame);
|
||||
}
|
||||
|
||||
private void simulationLoop() {
|
||||
String[] randomCalls = {"PA0GUS", "F6APE", "OH8K", "OZ2M", "G4CBW"};
|
||||
String[] msgs = {"CQ 144.300", "Tnx for QSO", "Any sked?", "QRV 432.200"};
|
||||
Random rand = new Random();
|
||||
|
||||
while (running) {
|
||||
try {
|
||||
Thread.sleep(3000 + rand.nextInt(5000)); // 3-8 Sek Pause
|
||||
|
||||
int action = rand.nextInt(10);
|
||||
String call = randomCalls[rand.nextInt(randomCalls.length)];
|
||||
|
||||
if (action < 4) { // 40% Chat
|
||||
handleChatMessage(call, "SimOp", msgs[rand.nextInt(msgs.length)]);
|
||||
} else if (action < 7) { // 30% DX Spot
|
||||
handleDxSpot(call, randomCalls[rand.nextInt(randomCalls.length)], "144." + rand.nextInt(400));
|
||||
} else if (action == 8) { // Login Simulation UA5
|
||||
if (!onlineUsers.containsKey(call)) {
|
||||
addUser(call, "SimOp", "JO11");
|
||||
broadcast("UA5|" + CHAT_ID + "|" + call + "|SimOp|JO11|2|");
|
||||
}
|
||||
} else if (action == 9) { // Logout Simulation UR6
|
||||
if (onlineUsers.containsKey(call) && !call.equals("DK5EW")) { // DK5EW nicht kicken
|
||||
removeUser(call);
|
||||
broadcast("UR6|" + CHAT_ID + "|" + call + "|");
|
||||
}
|
||||
}
|
||||
|
||||
// Ping ab und zu
|
||||
if (rand.nextInt(5) == 0) broadcast("CK|");
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kleine Datenklasse
|
||||
private static class User {
|
||||
String call, name, loc;
|
||||
User(String c, String n, String l) { this.call=c; this.name=n; this.loc=l; }
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package kst4contest.view.map;
|
||||
|
||||
public class mapTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
TerrainPackageService terrainPackageService = new TerrainPackageService();
|
||||
|
||||
TerrainPackageService.TerrainPreparationResult result =
|
||||
terrainPackageService.prepareTerrainForLocators(
|
||||
"JO51IJ",
|
||||
"JO22JK",
|
||||
"TEST",
|
||||
144.300,
|
||||
10.0,
|
||||
10.0,
|
||||
""
|
||||
);
|
||||
|
||||
System.out.println(result.success());
|
||||
System.out.println(result.message());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.utils.ApplicationFileUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Opens databases of older schema generations with a {@link DBController} and verifies that
|
||||
* the controller migrates them to the current schema: all columns present, PRAGMA
|
||||
* user_version at the current level, legacy callsign keys merged to base callsigns and the
|
||||
* existing rows preserved. The fixtures are built with plain sqlite-jdbc so the test does not
|
||||
* depend on the controller to create "old" files.
|
||||
*/
|
||||
class DBControllerSchemaMigrationTest {
|
||||
|
||||
private static final String USER_HOME_PROPERTY = "user.home";
|
||||
|
||||
/** Columns the current ChatMember CREATE TABLE statement in DBController declares. */
|
||||
private static final List<String> CHAT_MEMBER_COLUMNS = List.of(
|
||||
"callsign", "qra", "name", "lastActivityDateTime",
|
||||
"worked", "worked144", "worked432", "worked1240", "worked2300", "worked3400",
|
||||
"worked5600", "worked10G", "worked50", "worked70",
|
||||
"notQRV144", "notQRV432", "notQRV1240", "notQRV2300", "notQRV3400", "notQRV5600",
|
||||
"notQRV10G", "notQRV50", "notQRV70",
|
||||
"lastFlagsChangeEpochMs");
|
||||
|
||||
/** Columns the current WorkedGrossField CREATE TABLE statement in DBController declares. */
|
||||
private static final List<String> WORKED_GROSS_FIELD_COLUMNS = List.of(
|
||||
"band", "grossField", "locator", "callsign", "source", "lastWorkedEpochMs");
|
||||
|
||||
/**
|
||||
* ChatMember as created by release 1.1: no not-QRV columns, no timestamp, no 50/70 MHz.
|
||||
*/
|
||||
private static final String CREATE_CHAT_MEMBER_V11 =
|
||||
"CREATE TABLE ChatMember ("
|
||||
+ "callsign TEXT NOT NULL PRIMARY KEY, qra TEXT, name TEXT, "
|
||||
+ "lastActivityDateTime TEXT, "
|
||||
+ "worked BOOLEAN DEFAULT 0, worked144 BOOLEAN DEFAULT 0, "
|
||||
+ "worked432 BOOLEAN DEFAULT 0, worked1240 BOOLEAN DEFAULT 0, "
|
||||
+ "worked2300 BOOLEAN DEFAULT 0, worked3400 BOOLEAN DEFAULT 0, "
|
||||
+ "worked5600 BOOLEAN DEFAULT 0, worked10G BOOLEAN DEFAULT 0);";
|
||||
|
||||
/**
|
||||
* ChatMember as created by release 1.3: not-QRV columns and the expiry timestamp exist,
|
||||
* the 50/70 MHz columns of release 1.4 do not.
|
||||
*/
|
||||
private static final String CREATE_CHAT_MEMBER_V13 =
|
||||
"CREATE TABLE ChatMember ("
|
||||
+ "callsign TEXT NOT NULL PRIMARY KEY, qra TEXT, name TEXT, "
|
||||
+ "lastActivityDateTime TEXT, "
|
||||
+ "worked BOOLEAN DEFAULT 0, worked144 BOOLEAN DEFAULT 0, "
|
||||
+ "worked432 BOOLEAN DEFAULT 0, worked1240 BOOLEAN DEFAULT 0, "
|
||||
+ "worked2300 BOOLEAN DEFAULT 0, worked3400 BOOLEAN DEFAULT 0, "
|
||||
+ "worked5600 BOOLEAN DEFAULT 0, worked10G BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV144 BOOLEAN DEFAULT 0, notQRV432 BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV1240 BOOLEAN DEFAULT 0, notQRV2300 BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV3400 BOOLEAN DEFAULT 0, notQRV5600 BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV10G BOOLEAN DEFAULT 0, "
|
||||
+ "lastFlagsChangeEpochMs INTEGER DEFAULT 0);";
|
||||
|
||||
@TempDir
|
||||
Path temporaryHomeDirectory;
|
||||
|
||||
private String originalUserHome;
|
||||
private DBController controller;
|
||||
|
||||
@BeforeEach
|
||||
void redirectUserHomeToTemporaryDirectory() {
|
||||
originalUserHome = System.getProperty(USER_HOME_PROPERTY);
|
||||
System.setProperty(USER_HOME_PROPERTY, temporaryHomeDirectory.toString());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void closeControllerAndRestoreUserHome() {
|
||||
if (controller != null) {
|
||||
controller.closeDBConnection();
|
||||
controller = null;
|
||||
}
|
||||
if (originalUserHome == null) {
|
||||
System.clearProperty(USER_HOME_PROPERTY);
|
||||
} else {
|
||||
System.setProperty(USER_HOME_PROPERTY, originalUserHome);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void seededTemplateDatabaseIsMigratedToTheCurrentSchema() throws Exception {
|
||||
controller = new DBController("praktiKST.db", true);
|
||||
String databasePath = controller.getDatabaseFilePath();
|
||||
|
||||
assertEquals(currentSchemaVersion(), userVersion(databasePath));
|
||||
assertColumns(databasePath, "ChatMember", CHAT_MEMBER_COLUMNS);
|
||||
assertColumns(databasePath, "WorkedGrossField", WORKED_GROSS_FIELD_COLUMNS);
|
||||
assertEquals(0, legacyCallsignRowCount(databasePath),
|
||||
"the normalisation rebuild must leave no slash or dash keys behind");
|
||||
|
||||
Map<String, ChatMember> stations = controller.fetchChatMemberWkdDataFromDB();
|
||||
// The bundled template carries 3452 foreign callsigns, 199 of them with a suffix or
|
||||
// prefix; merging those to their base callsign shrinks the table but keeps the stations.
|
||||
assertTrue(stations.size() > 3000 && stations.size() < 3452,
|
||||
"unexpected station count after the rebuild: " + stations.size());
|
||||
|
||||
// "OE/HA5DDX" is the only row of this station in the template.
|
||||
ChatMember ha5ddx = stations.get("HA5DDX");
|
||||
assertNotNull(ha5ddx, "OE/HA5DDX must be stored under its base callsign");
|
||||
assertEquals("JN77WM", ha5ddx.getQra());
|
||||
assertEquals("Shani", ha5ddx.getName());
|
||||
assertFalse(stations.containsKey("OE/HA5DDX"));
|
||||
|
||||
// "F6KFH" and "F6KFH-2" both exist in the template and collapse into one row.
|
||||
assertTrue(stations.containsKey("F6KFH"));
|
||||
assertFalse(stations.containsKey("F6KFH-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void release11SchemaGainsAllColumnsAndMergesLegacyCallsignKeys() throws Exception {
|
||||
String relativeName = "profiles/legacy11/praktiKST.db";
|
||||
String databasePath = fixturePath(relativeName);
|
||||
try (Connection raw = openRaw(databasePath); Statement statement = raw.createStatement()) {
|
||||
statement.executeUpdate(CREATE_CHAT_MEMBER_V11);
|
||||
statement.executeUpdate("INSERT INTO ChatMember (callsign, qra, name, worked144) "
|
||||
+ "VALUES ('OK2M-70', 'JN69UN', 'Team', 1);");
|
||||
statement.executeUpdate("INSERT INTO ChatMember (callsign, worked432) "
|
||||
+ "VALUES ('OK2M', 1);");
|
||||
statement.executeUpdate("INSERT INTO ChatMember (callsign, qra, name, worked) "
|
||||
+ "VALUES ('EA5/G8MBI/P', 'IM98PA', 'Bob', 1);");
|
||||
statement.executeUpdate("INSERT INTO ChatMember (callsign, qra, name) "
|
||||
+ "VALUES ('DL1XYZ', 'JO31AA', 'Plain');");
|
||||
}
|
||||
assertEquals(0, userVersion(databasePath));
|
||||
|
||||
controller = new DBController(relativeName, false);
|
||||
|
||||
assertEquals(currentSchemaVersion(), userVersion(databasePath));
|
||||
assertColumns(databasePath, "ChatMember", CHAT_MEMBER_COLUMNS);
|
||||
assertColumns(databasePath, "WorkedGrossField", WORKED_GROSS_FIELD_COLUMNS);
|
||||
assertEquals(0, legacyCallsignRowCount(databasePath));
|
||||
|
||||
Map<String, ChatMember> stations = controller.fetchChatMemberWkdDataFromDB();
|
||||
assertEquals(Set.of("DL1XYZ", "G8MBI", "OK2M"), new TreeSet<>(stations.keySet()));
|
||||
|
||||
ChatMember ok2m = stations.get("OK2M");
|
||||
assertTrue(ok2m.isWorked144(), "worked144 of OK2M-70 must survive the merge");
|
||||
assertTrue(ok2m.isWorked432(), "worked432 of OK2M must survive the merge");
|
||||
assertEquals("JN69UN", ok2m.getQra(), "the merge fills an empty locator from the other row");
|
||||
assertEquals("Team", ok2m.getName());
|
||||
assertTrue(ok2m.isQrv144() && ok2m.isQrv50() && ok2m.isQrv70(),
|
||||
"columns added by the migration default to QRV");
|
||||
|
||||
ChatMember g8mbi = stations.get("G8MBI");
|
||||
assertTrue(g8mbi.isWorked());
|
||||
assertEquals("IM98PA", g8mbi.getQra());
|
||||
|
||||
ChatMember dl1xyz = stations.get("DL1XYZ");
|
||||
assertFalse(dl1xyz.isWorked());
|
||||
assertFalse(dl1xyz.isWorked144());
|
||||
assertEquals("Plain", dl1xyz.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void release13SchemaGainsBandColumnsAndKeepsRowsUntouched() throws Exception {
|
||||
String relativeName = "profiles/legacy13/praktiKST.db";
|
||||
String databasePath = fixturePath(relativeName);
|
||||
long recentFlagsChange = System.currentTimeMillis() - 60_000L;
|
||||
try (Connection raw = openRaw(databasePath); Statement statement = raw.createStatement()) {
|
||||
statement.executeUpdate(CREATE_CHAT_MEMBER_V13);
|
||||
statement.executeUpdate("INSERT INTO ChatMember (callsign, qra, name, "
|
||||
+ "lastActivityDateTime, worked144, notQRV432, lastFlagsChangeEpochMs) "
|
||||
+ "VALUES ('DL2ABC', 'JO62QL', 'Anna', 'Sun Aug 06 10:21:23 CEST 2023', 1, 1, "
|
||||
+ recentFlagsChange + ");");
|
||||
statement.executeUpdate("INSERT INTO ChatMember (callsign) VALUES ('DL3DEF');");
|
||||
}
|
||||
|
||||
controller = new DBController(relativeName, false);
|
||||
|
||||
assertEquals(currentSchemaVersion(), userVersion(databasePath));
|
||||
assertColumns(databasePath, "ChatMember", CHAT_MEMBER_COLUMNS);
|
||||
|
||||
Map<String, ChatMember> stations = controller.fetchChatMemberWkdDataFromDB();
|
||||
assertEquals(Set.of("DL2ABC", "DL3DEF"), new TreeSet<>(stations.keySet()));
|
||||
|
||||
ChatMember dl2abc = stations.get("DL2ABC");
|
||||
assertTrue(dl2abc.isWorked144());
|
||||
assertFalse(dl2abc.isQrv432());
|
||||
assertTrue(dl2abc.isQrv144());
|
||||
assertFalse(dl2abc.isWorked50());
|
||||
assertFalse(dl2abc.isWorked70());
|
||||
assertTrue(dl2abc.isQrv50());
|
||||
assertTrue(dl2abc.isQrv70());
|
||||
assertEquals(recentFlagsChange, dl2abc.getLastFlagsChangeEpochMs(),
|
||||
"a recent flags timestamp must not be reset by the expiry cleanup");
|
||||
|
||||
// Without legacy keys no rebuild happens, so even the column the controller never
|
||||
// reads back stays as it was.
|
||||
assertEquals("Sun Aug 06 10:21:23 CEST 2023",
|
||||
scalar(databasePath, "SELECT lastActivityDateTime FROM ChatMember WHERE callsign = 'DL2ABC';"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reopeningAnUpToDateDatabaseChangesNothing() throws Exception {
|
||||
String relativeName = "profiles/current/praktiKST.db";
|
||||
controller = new DBController(relativeName, false);
|
||||
String databasePath = controller.getDatabaseFilePath();
|
||||
|
||||
ChatMember worked = new ChatMember();
|
||||
worked.setCallSign("DL0XYZ");
|
||||
worked.setQra("JO51IJ");
|
||||
worked.setWorked(true);
|
||||
worked.setWorked144(true);
|
||||
controller.storeChatMember(worked);
|
||||
controller.closeDBConnection();
|
||||
controller = null;
|
||||
|
||||
assertEquals(currentSchemaVersion(), userVersion(databasePath));
|
||||
List<String> before = dump(databasePath);
|
||||
|
||||
controller = new DBController(relativeName, false);
|
||||
assertTrue(controller.fetchChatMemberWkdDataFromDB().get("DL0XYZ").isWorked144());
|
||||
controller.closeDBConnection();
|
||||
controller = null;
|
||||
|
||||
assertEquals(before, dump(databasePath),
|
||||
"schema, user_version and rows must be identical after reopening");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// helpers
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
private static int currentSchemaVersion() throws ReflectiveOperationException {
|
||||
Field field = DBController.class.getDeclaredField("CURRENT_DATABASE_SCHEMA_VERSION");
|
||||
field.setAccessible(true);
|
||||
return field.getInt(null);
|
||||
}
|
||||
|
||||
private static String fixturePath(String relativeName) throws Exception {
|
||||
String path = ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, relativeName);
|
||||
Files.createDirectories(Path.of(path).getParent());
|
||||
return path;
|
||||
}
|
||||
|
||||
private static Connection openRaw(String databasePath) throws SQLException {
|
||||
return DriverManager.getConnection("jdbc:sqlite:" + databasePath);
|
||||
}
|
||||
|
||||
private static int userVersion(String databasePath) throws SQLException {
|
||||
return Integer.parseInt(scalar(databasePath, "PRAGMA user_version;"));
|
||||
}
|
||||
|
||||
private static String scalar(String databasePath, String sql) throws SQLException {
|
||||
try (Connection raw = openRaw(databasePath);
|
||||
Statement statement = raw.createStatement();
|
||||
ResultSet resultSet = statement.executeQuery(sql)) {
|
||||
assertTrue(resultSet.next(), "no row for " + sql);
|
||||
return resultSet.getString(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static int legacyCallsignRowCount(String databasePath) throws SQLException {
|
||||
return Integer.parseInt(scalar(databasePath,
|
||||
"SELECT count(*) FROM ChatMember WHERE callsign LIKE '%/%' OR callsign LIKE '%-%';"));
|
||||
}
|
||||
|
||||
private static void assertColumns(String databasePath, String table, List<String> expected)
|
||||
throws SQLException {
|
||||
Set<String> actual = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try (Connection raw = openRaw(databasePath);
|
||||
Statement statement = raw.createStatement();
|
||||
ResultSet resultSet = statement.executeQuery("PRAGMA table_info(" + table + ");")) {
|
||||
while (resultSet.next()) {
|
||||
actual.add(resultSet.getString("name"));
|
||||
}
|
||||
}
|
||||
List<String> missing = new ArrayList<>();
|
||||
for (String column : expected) {
|
||||
if (!actual.contains(column)) {
|
||||
missing.add(column);
|
||||
}
|
||||
}
|
||||
assertEquals(List.of(), missing, "columns missing in " + table + "; present: " + actual);
|
||||
}
|
||||
|
||||
/** Captures schema, user_version and every row, so two states can be compared. */
|
||||
private static List<String> dump(String databasePath) throws SQLException {
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("user_version=" + userVersion(databasePath));
|
||||
try (Connection raw = openRaw(databasePath); Statement statement = raw.createStatement()) {
|
||||
try (ResultSet resultSet = statement.executeQuery(
|
||||
"SELECT type, name, sql FROM sqlite_master ORDER BY type, name;")) {
|
||||
while (resultSet.next()) {
|
||||
lines.add(resultSet.getString(1) + " " + resultSet.getString(2) + " " + resultSet.getString(3));
|
||||
}
|
||||
}
|
||||
for (String table : List.of("ChatMember", "WorkedGrossField")) {
|
||||
try (ResultSet resultSet = statement.executeQuery("SELECT * FROM " + table + " ORDER BY 1;")) {
|
||||
int columns = resultSet.getMetaData().getColumnCount();
|
||||
while (resultSet.next()) {
|
||||
StringBuilder row = new StringBuilder(table);
|
||||
for (int i = 1; i <= columns; i++) {
|
||||
row.append('|').append(resultSet.getString(i));
|
||||
}
|
||||
lines.add(row.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
-2
@@ -1,7 +1,5 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
File renamed without changes.
File renamed without changes.
@@ -31,7 +31,7 @@ class ChatPreferencesObservableTest {
|
||||
void theListSettingsStartEmpty() {
|
||||
ChatPreferences prefs = new ChatPreferences();
|
||||
|
||||
assertTrue(prefs.getLstNotify_QSOSniffer_sniffedWordsList().snapshot().isEmpty());
|
||||
assertTrue(prefs.getLstNotify_QSOSniffer_sniffedCallSignList().snapshot().isEmpty());
|
||||
assertTrue(prefs.getLstNotify_QSOSniffer_sniffedPrefixLocList().snapshot().isEmpty());
|
||||
assertTrue(prefs.getLst_txtSnipList().snapshot().isEmpty());
|
||||
assertTrue(prefs.getLst_txtShortCutBtnList().snapshot().isEmpty());
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
package kst4contest.model;
|
||||
|
||||
import kst4contest.observe.MutableValue;
|
||||
import kst4contest.observe.ObservableRoster;
|
||||
import kst4contest.observe.SimpleValue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Drives every public single-argument setter of {@link ChatPreferences} to a value that
|
||||
* differs from the compiled-in default, writes the preferences to XML, reads them into a
|
||||
* fresh instance and compares every getter. The setters are discovered by reflection so a
|
||||
* newly added preference is covered automatically; a field that is not persisted on purpose
|
||||
* must be listed in {@link #NOT_PERSISTED_SETTERS} with a reason.
|
||||
*/
|
||||
class ChatPreferencesRoundTripTest {
|
||||
|
||||
/**
|
||||
* Setters whose value is intentionally not written to the XML file. Each entry names the
|
||||
* evidence in {@code ChatPreferences}. A field that is lost by accident must not be added
|
||||
* here silently; a known persistence bug is listed with a "BUG:" prefix so the test stays
|
||||
* green while the report stays loud.
|
||||
*/
|
||||
private static final Map<String, String> NOT_PERSISTED_SETTERS = Map.ofEntries(
|
||||
Map.entry("setStoreAndRestorePreferencesFileName",
|
||||
"Path of the XML file itself; set by the constructors, never a child "
|
||||
+ "element of <praktiKST>"),
|
||||
Map.entry("setChatState",
|
||||
"Declared as 'working variable only for use by primarystage (title bar)'; "
|
||||
+ "writePreferencesToXmlFile never emits it"),
|
||||
Map.entry("setProgramVersion",
|
||||
"Compiled-in about text; writePreferencesToXmlFile never emits it"),
|
||||
Map.entry("setActualQTF",
|
||||
"Runtime antenna direction ('will be updated by user at runtime!'); "
|
||||
+ "writePreferencesToXmlFile never emits it"),
|
||||
Map.entry("setMessageHandling_autoAnswerTextSecondCat",
|
||||
"Written as <autoAnswerTextSecondCat>, but readPreferencesFromXmlFile "
|
||||
+ "deliberately mirrors the main-category text over it because the UI "
|
||||
+ "exposes one shared auto-answer setting (see the comment above "
|
||||
+ "'messageHandling_autoAnswerTextSecondCat = "
|
||||
+ "messageHandling_autoAnswerTextMainCat')"),
|
||||
Map.entry("setMessageHandling_autoAnswerEnabledSecondCat",
|
||||
"Same deliberate mirroring as the second-category auto-answer text; the "
|
||||
+ "main-category flag wins after every read"),
|
||||
Map.entry("setBcn_contestScoreSum",
|
||||
"DEAD FIELD: declared with accessors only, no caller in core or app-desktop, "
|
||||
+ "never written nor read")
|
||||
);
|
||||
|
||||
/**
|
||||
* Setters that are an alias for another setter and therefore have no getter of their own.
|
||||
* The canonical setter is covered by the reflective loop.
|
||||
*/
|
||||
private static final Map<String, String> ALIAS_SETTERS = Map.ofEntries(
|
||||
Map.entry("setLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup",
|
||||
"Capital-S alias of setLogsynch_storeWorkedCallSignsFileNameUDPMessageBackup")
|
||||
);
|
||||
|
||||
/**
|
||||
* Setters whose argument is validated or parsed, so the generic "distinctive value per
|
||||
* type" would be rejected. The value here is a valid, non-default one.
|
||||
*/
|
||||
private static final Map<String, Object> SPECIAL_VALUES = Map.ofEntries(
|
||||
Map.entry("setGuiOptions_language", "de"),
|
||||
Map.entry("setMYQRGFirstCat", "144.321.00"),
|
||||
Map.entry("setMYQRGSecondCat", "432.321.00"),
|
||||
Map.entry("setAirScout_asBandString", "4320000")
|
||||
);
|
||||
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void everySetterSurvivesTheXmlRoundTrip() throws Exception {
|
||||
Path preferencesFile = temporaryDirectory.resolve("roundtrip.xml");
|
||||
ChatPreferences defaults = new ChatPreferences(preferencesFile);
|
||||
ChatPreferences written = new ChatPreferences(preferencesFile);
|
||||
|
||||
List<Method> setters = settersUnderTest();
|
||||
assertTrue(setters.size() > 100,
|
||||
"Expected the reflective scan to find the ~130 setters, found " + setters.size());
|
||||
|
||||
List<String> unbuildable = new ArrayList<>();
|
||||
List<String> withoutGetter = new ArrayList<>();
|
||||
List<String> unchangedByValue = new ArrayList<>();
|
||||
Map<Method, Method> getterBySetter = new LinkedHashMap<>();
|
||||
|
||||
for (Method setter : setters) {
|
||||
Method getter = findGetter(setter);
|
||||
if (getter == null) {
|
||||
withoutGetter.add(setter.getName());
|
||||
continue;
|
||||
}
|
||||
Object valueToSet = distinctiveValue(setter, getter.invoke(defaults));
|
||||
if (valueToSet == null) {
|
||||
unbuildable.add(setter.getName() + "(" + setter.getParameterTypes()[0].getSimpleName() + ")");
|
||||
continue;
|
||||
}
|
||||
setter.invoke(written, valueToSet);
|
||||
getterBySetter.put(setter, getter);
|
||||
|
||||
if (sameValue(getter.invoke(defaults), getter.invoke(written))) {
|
||||
unchangedByValue.add(setter.getName() + " still reads " + describe(getter.invoke(written)));
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(List.of(), withoutGetter,
|
||||
"Setters without a matching getter (add them to ALIAS_SETTERS with a reason)");
|
||||
assertEquals(List.of(), unbuildable,
|
||||
"Setters whose parameter type this test cannot construct");
|
||||
assertEquals(List.of(), unchangedByValue,
|
||||
"Setters whose distinctive value equals the default (the round trip could not "
|
||||
+ "detect a loss for them)");
|
||||
|
||||
assertTrue(written.writePreferencesToXmlFile(), "writing " + preferencesFile + " failed");
|
||||
|
||||
ChatPreferences restored = new ChatPreferences(preferencesFile);
|
||||
assertTrue(restored.readPreferencesFromXmlFile(), "reading " + preferencesFile + " failed");
|
||||
|
||||
List<String> lost = new ArrayList<>();
|
||||
for (Map.Entry<Method, Method> entry : getterBySetter.entrySet()) {
|
||||
Method getter = entry.getValue();
|
||||
Object expected = getter.invoke(written);
|
||||
Object actual = getter.invoke(restored);
|
||||
if (!sameValue(expected, actual)) {
|
||||
lost.add(entry.getKey().getName() + ": written=" + describe(expected)
|
||||
+ " restored=" + describe(actual));
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(List.of(), lost,
|
||||
"Fields that did not survive writePreferencesToXmlFile -> readPreferencesFromXmlFile");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bundledTemplatePreferencesLoadWithKnownValues() throws IOException {
|
||||
Path templateCopy = temporaryDirectory.resolve("template.xml");
|
||||
try (InputStream template = ChatPreferences.class
|
||||
.getResourceAsStream(ChatPreferences.PREFERENCE_RESOURCE)) {
|
||||
assertNotNull(template, "bundled resource " + ChatPreferences.PREFERENCE_RESOURCE);
|
||||
Files.copy(template, templateCopy, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
ChatPreferences fromTemplate = new ChatPreferences(templateCopy);
|
||||
assertTrue(fromTemplate.readPreferencesFromXmlFile());
|
||||
|
||||
// Values the seeded template carries today; the station block uses the legacy tag names.
|
||||
assertEquals("DO5AMF", fromTemplate.getStn_loginCallSign());
|
||||
assertEquals("JO54ME", fromTemplate.getStn_loginLocatorMainCat());
|
||||
assertEquals(2, fromTemplate.getLoginChatCategoryMain().getCategoryNumber());
|
||||
assertEquals(3, fromTemplate.getLoginChatCategorySecond().getCategoryNumber());
|
||||
assertTrue(fromTemplate.isLoginToSecondChatEnabled());
|
||||
assertEquals(1333.0, fromTemplate.getStn_maxQRBDefault());
|
||||
assertEquals(12060, fromTemplate.getLogsynch_ucxUDPWkdCallListenerPort());
|
||||
assertEquals(8000, fromTemplate.getNotify_dxclusterServerPort());
|
||||
assertEquals(9872, fromTemplate.getAirScout_asCommunicationPort());
|
||||
assertTrue(fromTemplate.getLst_txtShortCutBtnList().snapshot().contains("sked"));
|
||||
assertTrue(fromTemplate.isStn_bandActive144());
|
||||
assertFalse(fromTemplate.isStn_bandActive10G());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyFileWithoutConfigVersionMapsOldTagNamesOntoCurrentFields() throws IOException {
|
||||
Path legacyFile = temporaryDirectory.resolve("legacy.xml");
|
||||
Files.writeString(legacyFile, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST>
|
||||
<station>
|
||||
<LoginCallSign>DL1ABC-2</LoginCallSign>
|
||||
<LoginPassword>secret</LoginPassword>
|
||||
<LoginDisplayedName>Old Name</LoginDisplayedName>
|
||||
<LoginLocator>JO31AA</LoginLocator>
|
||||
<ChatCategory>3</ChatCategory>
|
||||
</station>
|
||||
<notifications>
|
||||
<notify_dxclusterServerPort>8123</notify_dxclusterServerPort>
|
||||
</notifications>
|
||||
</praktiKST>
|
||||
""");
|
||||
|
||||
ChatPreferences legacy = new ChatPreferences(legacyFile);
|
||||
assertTrue(legacy.readPreferencesFromXmlFile());
|
||||
|
||||
assertEquals("DL1ABC-2", legacy.getStn_loginCallSign());
|
||||
assertEquals("secret", legacy.getStn_loginPassword());
|
||||
assertEquals("Old Name", legacy.getStn_loginNameMainCat());
|
||||
assertEquals("JO31AA", legacy.getStn_loginLocatorMainCat());
|
||||
assertEquals(3, legacy.getLoginChatCategoryMain().getCategoryNumber());
|
||||
assertEquals(8123, legacy.getNotify_dxclusterServerPort());
|
||||
// A tag that is missing keeps the compiled-in default.
|
||||
assertEquals(new ChatPreferences(legacyFile).getStn_on4kstServersPort(),
|
||||
legacy.getStn_on4kstServersPort());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// reflection helpers
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
private static List<Method> settersUnderTest() {
|
||||
List<Method> setters = new ArrayList<>();
|
||||
for (Method method : ChatPreferences.class.getMethods()) {
|
||||
if (method.getDeclaringClass() != ChatPreferences.class
|
||||
|| Modifier.isStatic(method.getModifiers())
|
||||
|| !method.getName().startsWith("set")
|
||||
|| method.getParameterCount() != 1
|
||||
|| NOT_PERSISTED_SETTERS.containsKey(method.getName())
|
||||
|| ALIAS_SETTERS.containsKey(method.getName())) {
|
||||
continue;
|
||||
}
|
||||
setters.add(method);
|
||||
}
|
||||
setters.sort(Comparator.comparing(Method::getName));
|
||||
return setters;
|
||||
}
|
||||
|
||||
private static Method findGetter(Method setter) {
|
||||
String property = setter.getName().substring(3);
|
||||
for (String prefix : new String[] {"get", "is"}) {
|
||||
try {
|
||||
return ChatPreferences.class.getMethod(prefix + property);
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
// try the next prefix
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a value that differs from the current one. Returns null when the parameter type
|
||||
* cannot be constructed here.
|
||||
*/
|
||||
private static Object distinctiveValue(Method setter, Object currentValue) {
|
||||
Object special = SPECIAL_VALUES.get(setter.getName());
|
||||
if (special != null) {
|
||||
return special;
|
||||
}
|
||||
|
||||
Class<?> type = setter.getParameterTypes()[0];
|
||||
String name = setter.getName().substring(3);
|
||||
Object current = unwrap(currentValue);
|
||||
|
||||
if (type == boolean.class) {
|
||||
return !(current instanceof Boolean b && b);
|
||||
}
|
||||
if (type == int.class) {
|
||||
return (current instanceof Integer i ? i : 0) + 7;
|
||||
}
|
||||
if (type == long.class) {
|
||||
return (current instanceof Long l ? l : 0L) + 7L;
|
||||
}
|
||||
if (type == double.class) {
|
||||
return (current instanceof Double d ? d : 0.0) + 7.5;
|
||||
}
|
||||
if (type == String.class) {
|
||||
return "rt-" + name;
|
||||
}
|
||||
if (type == double[].class) {
|
||||
double[] base = current instanceof double[] array && array.length > 0
|
||||
? array : new double[] {0.0, 0.0};
|
||||
double[] changed = new double[base.length];
|
||||
for (int i = 0; i < base.length; i++) {
|
||||
// Window positions default to NaN ("not yet placed"); NaN + 7 is still NaN.
|
||||
changed[i] = Double.isNaN(base[i]) ? 100.0 + i : base[i] + 7.0 + i;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
if (Collection.class.isAssignableFrom(type)) {
|
||||
return List.of("rt-" + name + "-a", "rt-" + name + "-b");
|
||||
}
|
||||
if (type == ChatCategory.class) {
|
||||
int currentNumber = current instanceof Integer i ? i : 2;
|
||||
return new ChatCategory(currentNumber == 5 ? 6 : 5);
|
||||
}
|
||||
if (type == MutableValue.class) {
|
||||
return new SimpleValue<>(current instanceof Double d ? d + 7.5 : 7.5);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Reduces observable wrappers and domain objects to plain comparable values. */
|
||||
private static Object unwrap(Object value) {
|
||||
if (value instanceof MutableValue<?> mutable) {
|
||||
return mutable.get();
|
||||
}
|
||||
if (value instanceof ObservableRoster<?> roster) {
|
||||
return roster.snapshot();
|
||||
}
|
||||
if (value instanceof ChatCategory category) {
|
||||
return category.getCategoryNumber();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean sameValue(Object left, Object right) {
|
||||
Object a = unwrap(left);
|
||||
Object b = unwrap(right);
|
||||
if (a instanceof double[] x && b instanceof double[] y) {
|
||||
return Arrays.equals(x, y);
|
||||
}
|
||||
if (a instanceof Double x && b instanceof Double y) {
|
||||
return Math.abs(x - y) < 1e-9;
|
||||
}
|
||||
return Objects.equals(a, b);
|
||||
}
|
||||
|
||||
private static String describe(Object value) {
|
||||
Object plain = unwrap(value);
|
||||
if (plain instanceof double[] array) {
|
||||
return Arrays.toString(array);
|
||||
}
|
||||
return String.valueOf(plain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package kst4contest.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Guards the QSO sniffer callsign list against the historical {@code <snifferWords>} block.
|
||||
* Older preference files still carry that element; its entries must not end up in the
|
||||
* monitored-callsign list (issue #103).
|
||||
*/
|
||||
class ChatPreferencesSnifferListTest {
|
||||
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void legacySnifferWordsDoNotLeakIntoTheCallsignList() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("sniffer.xml");
|
||||
Files.writeString(preferencesFile, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST>
|
||||
<snifferWords>
|
||||
<w>DL1ABC</w>
|
||||
</snifferWords>
|
||||
<snifferCallSigns>
|
||||
<callSign>DN9APW-2</callSign>
|
||||
</snifferCallSigns>
|
||||
</praktiKST>
|
||||
""");
|
||||
|
||||
ChatPreferences preferences = new ChatPreferences(preferencesFile);
|
||||
assertTrue(preferences.readPreferencesFromXmlFile());
|
||||
|
||||
assertEquals(List.of("DN9APW"),
|
||||
preferences.getLstNotify_QSOSniffer_sniffedCallSignList().snapshot(),
|
||||
"only the <snifferCallSigns> entry, normalised to its base callsign, "
|
||||
+ "may be monitored; <snifferWords> is an ignored legacy element");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ChatPreferencesStationMapVisibilityTest {
|
||||
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void pathAnalysisIsVisibleByDefault() {
|
||||
ChatPreferences preferences = new ChatPreferences();
|
||||
|
||||
assertTrue(preferences.isGUIstationMapPathAnalysisVisible());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hiddenPathAnalysisStateSurvivesXmlRoundTrip() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("preferences.xml");
|
||||
|
||||
ChatPreferences writtenPreferences = new ChatPreferences();
|
||||
writtenPreferences.setStoreAndRestorePreferencesFileName(
|
||||
preferencesFile.toString());
|
||||
writtenPreferences.setGUIstationMapPathAnalysisVisible(false);
|
||||
writtenPreferences.writePreferencesToXmlFile();
|
||||
|
||||
String writtenXml = Files.readString(preferencesFile);
|
||||
assertTrue(writtenXml.contains(
|
||||
"<GUIstationMapPathAnalysisVisible>false"
|
||||
+ "</GUIstationMapPathAnalysisVisible>"));
|
||||
|
||||
ChatPreferences restoredPreferences = new ChatPreferences();
|
||||
restoredPreferences.setStoreAndRestorePreferencesFileName(
|
||||
preferencesFile.toString());
|
||||
restoredPreferences.readPreferencesFromXmlFile();
|
||||
|
||||
assertFalse(restoredPreferences.isGUIstationMapPathAnalysisVisible());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyXmlWithoutVisibilitySettingKeepsAnalysisDiscoverable()
|
||||
throws IOException {
|
||||
|
||||
Path legacyPreferencesFile =
|
||||
temporaryDirectory.resolve("legacy-preferences.xml");
|
||||
|
||||
Files.writeString(legacyPreferencesFile, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST>
|
||||
<configVersion>4</configVersion>
|
||||
<guiOptions>
|
||||
<GUIstationMapStageSceneSizeHW>1000.0;800.0</GUIstationMapStageSceneSizeHW>
|
||||
</guiOptions>
|
||||
</praktiKST>
|
||||
""");
|
||||
|
||||
ChatPreferences restoredPreferences = new ChatPreferences();
|
||||
restoredPreferences.setStoreAndRestorePreferencesFileName(
|
||||
legacyPreferencesFile.toString());
|
||||
restoredPreferences.readPreferencesFromXmlFile();
|
||||
|
||||
assertTrue(restoredPreferences.isGUIstationMapPathAnalysisVisible());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
import kst4contest.model.Band;
|
||||
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.assertTrue;
|
||||
|
||||
class FrequencyTextParserTest {
|
||||
|
||||
@Test
|
||||
void detectsExplicitStationNameFrequencies() {
|
||||
List<FrequencyTextParser.DetectedFrequency> detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"Phil 432.357"
|
||||
);
|
||||
|
||||
assertEquals(1, detected.size());
|
||||
assertEquals(
|
||||
Band.B_432,
|
||||
detected.get(0).getBand()
|
||||
);
|
||||
assertEquals(
|
||||
432.357,
|
||||
detected.get(0).getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsCommaAndMicrowaveFrequencies() {
|
||||
List<FrequencyTextParser.DetectedFrequency> detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"QRV 1296,210 / 10368.100"
|
||||
);
|
||||
|
||||
assertEquals(2, detected.size());
|
||||
|
||||
assertEquals(
|
||||
Band.B_1296,
|
||||
detected.get(0).getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Band.B_10G,
|
||||
detected.get(1).getBand()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsFiftyAndSeventyMhzFrequencies() {
|
||||
assertEquals(
|
||||
Band.B_50,
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies("50.150")
|
||||
.get(0)
|
||||
.getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Band.B_70,
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies("70.200")
|
||||
.get(0)
|
||||
.getBand()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresRelativeAndAmbiguousValues() {
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"Mike .180"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"Mike 180"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"David 1.2"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"RST 599"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizesSubKhzNotation() {
|
||||
FrequencyTextParser.DetectedFrequency detected =
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"144.300.03"
|
||||
)
|
||||
.get(0);
|
||||
|
||||
assertEquals(Band.B_144, detected.getBand());
|
||||
assertEquals(
|
||||
144.30003,
|
||||
detected.getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.utils.ApplicationFileUtils;
|
||||
|
||||
public class TestApplicationFileUtils {
|
||||
public static final String DATABASE_FILE = "praktiKST.db";
|
||||
private static final String DB_PATH = ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, DATABASE_FILE);
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
System.out.println("DB Path = " + DB_PATH);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class TestChatPreferences {
|
||||
|
||||
@Test
|
||||
public static void main(String[] args) {
|
||||
|
||||
ChatPreferences prefs = new ChatPreferences();
|
||||
prefs.readPreferencesFromXmlFile(); //works
|
||||
|
||||
prefs.writePreferencesToXmlFile(); //works
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.controller.DBController;
|
||||
import kst4contest.controller.Utils4KST;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.utils.ApplicationFileUtils;
|
||||
|
||||
public class TestDBController {
|
||||
|
||||
public static final String DATABASE_FILE = "praktiKST.db";
|
||||
private static final String DB_PATH = ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, DATABASE_FILE);
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
|
||||
DBController dbc = DBController.getInstance();
|
||||
// dbc.initDBConnection();
|
||||
|
||||
ChatMember dummy = new ChatMember();
|
||||
dummy.setCallSign("DM5M");
|
||||
dummy.setQra("jo51ij");
|
||||
dummy.setName("Team Test");
|
||||
dummy.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
|
||||
dummy.setWorked5600(true);
|
||||
// dummy.setWorked432(true);
|
||||
|
||||
dbc.fetchChatMemberWkdDataForOnlyOneCallsignFromDB(dummy);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.locatorUtils.Angle;
|
||||
import kst4contest.locatorUtils.Latitude;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
import kst4contest.locatorUtils.Longitude;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
public class TestLocatorUtils {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Location location = new Location("JN49FL");
|
||||
Location location2 = new Location("JO51IJ");
|
||||
|
||||
// System.out.println(location.getDistanceKm(location2));
|
||||
// System.out.println(Location.getBearing(location, location2));
|
||||
|
||||
|
||||
System.out.println((new Location().getDistanceKmByTwoLocatorStrings("JN49FL", "kn02fx") + ""));
|
||||
// System.out.println((new Location().getBearing());
|
||||
// int test = 888.08;
|
||||
// System.out.println(test);
|
||||
|
||||
// String test = new Location().getDistanceKmByTwoLocatorStrings("JN49FL", "Jo51ij") + "";
|
||||
//
|
||||
// DecimalFormat df = new DecimalFormat("#.##");
|
||||
//
|
||||
// System.out.println(df.format(Double.parseDouble(test)));
|
||||
|
||||
// Angle angle = new Angle();
|
||||
// Latitude lat = new Latitude();
|
||||
// Longitude lon = new Longitude();
|
||||
//
|
||||
// location.setLatitude(lat);
|
||||
// location.setLongitude(lon);
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.controller.ReadUDPbyAirScoutMessageThread;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class TestReadUDPASListenerThread {
|
||||
|
||||
@Test
|
||||
public static void main(String[] args) {
|
||||
|
||||
// ReadUDPbyAirScoutMessageThread asUDPReader = new ReadUDPbyAirScoutMessageThread(9872, null, "AS", "KST");
|
||||
// asUDPReader.start();
|
||||
|
||||
String testThis;
|
||||
|
||||
// testThis = "ASNEAREST: \"AS\" \"KST\" \"2023-04-01 21:33:42Z,DO5AMF,JN49GL,G4CLA,IO92JL,9,VLG2PT,M,190,75,14,BAW809,M,250,50,18,BEL6CM,M,143,50,12,WZZ6719,M,148,50,11,KLM1678,M,313,75,22,TRA1B,M,271,75,20,SAS4728,M,125,75,9,RYR6TL,M,90,75,6,UAE10,S,96,50,6\"";
|
||||
// asUDPReader.processASUDPMessage(testThis);
|
||||
|
||||
|
||||
// ucxUDPReader.processUCXUDPMessage(testThis);
|
||||
|
||||
|
||||
// ucxUDPReader =new ReadUDPbyUCXMessageThread(12060);
|
||||
// ucxUDPReader.processUCXUDPMessage(testThis);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.controller.ReadUDPbyUCXMessageThread;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class TestReadUDPUCXListenerThread {
|
||||
|
||||
@Test
|
||||
public static void main(String[] args) {
|
||||
|
||||
// ReadUDPbyUCXMessageThread ucxUDPReader = new ReadUDPbyUCXMessageThread(12060);
|
||||
// ucxUDPReader.start();
|
||||
|
||||
|
||||
String testThis;
|
||||
|
||||
testThis = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
|
||||
+ "<contactinfo>\r\n"
|
||||
+ " <app>UcxLog</app>\r\n"
|
||||
+ " <contestname></contestname>\r\n"
|
||||
+ " <contestnr></contestnr>\r\n"
|
||||
+ " <timestamp>2022-10-30 11:51:00</timestamp>\r\n"
|
||||
+ " <mycall>DO5AMF</mycall>\r\n"
|
||||
+ " <band>144</band>\r\n"
|
||||
+ " <rxfreq>14400000</rxfreq>\r\n"
|
||||
+ " <txfreq>14400000</txfreq>\r\n"
|
||||
+ " <operator></operator>\r\n"
|
||||
+ " <mode>USB</mode>\r\n"
|
||||
+ " <call>DM1AO</call>\r\n"
|
||||
+ " <countryprefix></countryprefix>\r\n"
|
||||
+ " <wpxprefix></wpxprefix>\r\n"
|
||||
+ " <stationprefix></stationprefix>\r\n"
|
||||
+ " <continent>EU</continent>\r\n"
|
||||
+ " <snt>59</snt>\r\n"
|
||||
+ " <sntnr>021 JN49FK</sntnr>\r\n"
|
||||
+ " <rcv>59</rcv>\r\n"
|
||||
+ " <rcvnr>001 JO50LT</rcvnr>\r\n"
|
||||
+ " <gridsquare></gridsquare>\r\n"
|
||||
+ " <exchange1></exchange1>\r\n"
|
||||
+ " <section></section>\r\n"
|
||||
+ " <comment></comment>\r\n"
|
||||
+ " <qth></qth>\r\n"
|
||||
+ " <name></name>\r\n"
|
||||
+ " <power></power>\r\n"
|
||||
+ " <misctext></misctext>\r\n"
|
||||
+ " <zone>0</zone>\r\n"
|
||||
+ " <prec></prec>\r\n"
|
||||
+ " <ck>0</ck>\r\n"
|
||||
+ " <ismultiplierl></ismultiplierl>\r\n"
|
||||
+ " <ismultiplier2></ismultiplier2>\r\n"
|
||||
+ " <ismultiplier3></ismultiplier3>\r\n"
|
||||
+ " <points></points>\r\n"
|
||||
+ " <radionr>1</radionr>\r\n"
|
||||
+ " <run1run2>1</run1run2>\r\n"
|
||||
+ " <RoverLocation></RoverLocation>\r\n"
|
||||
+ " <RadioInterfaced>0</RadioInterfaced>\r\n"
|
||||
+ " <NetworkedCompNr>0</NetworkedCompNr>\r\n"
|
||||
+ " <IsOriginal>True</IsOriginal>\r\n"
|
||||
+ " <NetBiosName></NetBiosName>\r\n"
|
||||
+ " <IsRunQSO>0</IsRunQSO>\r\n"
|
||||
+ " <StationName>LAPTOP-05GRNFHI</StationName>\r\n"
|
||||
+ " <ID>0C73B5B3B82C4836</ID>\r\n"
|
||||
+ " <IsClaimedQso>True</IsClaimedQso>\r\n"
|
||||
+ "</contactinfo> ";
|
||||
// ucxUDPReader.processUCXUDPMessage(testThis);
|
||||
|
||||
|
||||
testThis ="<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
|
||||
+ "<RadioInfo>\r\n"
|
||||
+ " <app>UcxLog</app>\r\n"
|
||||
+ " <RadioNr>1</RadioNr>\r\n"
|
||||
+ " <Freq>2101100</Freq>\r\n"
|
||||
+ " <TXFreq>2101100</TXFreq>\r\n"
|
||||
+ " <Mode>CW</Mode>\r\n"
|
||||
+ " <IsSplit>False</IsSplit>\r\n"
|
||||
+ "</RadioInfo>";
|
||||
|
||||
// ucxUDPReader =new ReadUDPbyUCXMessageThread(12060);
|
||||
// ucxUDPReader.processUCXUDPMessage(testThis);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class UDPAirScoutSendMessages {
|
||||
|
||||
@Test
|
||||
public static void main(String args[]) {
|
||||
try {
|
||||
// String host = "www.java2s.com";
|
||||
// byte[] message = "ASWATCHLIST: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes();
|
||||
|
||||
|
||||
|
||||
// Get the internet address of the specified host
|
||||
// InetAddress address = InetAddress.getByName(host);^
|
||||
// byte[] byteAddress = "127.0.0.1".getBytes();
|
||||
// System.out.println(byteAddress.length);
|
||||
String host = "255.255.255.255";
|
||||
int port = 9872;
|
||||
|
||||
byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes();
|
||||
InetAddress address = InetAddress.getByName("255.255.255.255");
|
||||
|
||||
// Initialize a datagram packet with data and address
|
||||
DatagramPacket packet = new DatagramPacket(message, message.length,
|
||||
address, port);
|
||||
|
||||
|
||||
// Create a datagram socket, send the packet through it, close it.
|
||||
DatagramSocket dsocket = new DatagramSocket();
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.send(packet);
|
||||
dsocket.close();
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.observe.SimpleValue;
|
||||
import kst4contest.controller.Utils4KST;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class Utils4KSTFrequencyTest {
|
||||
|
||||
@Test
|
||||
void convertsCompleteFrequenciesAcrossAllSupportedPrefixLengths() {
|
||||
assertEquals("50200.0", convert("50.200", "144"));
|
||||
assertEquals("70250.0", convert("70,250", "144"));
|
||||
assertEquals("144205.0", convert("144.205", "432"));
|
||||
assertEquals("432088.0", convert("432.088", "144"));
|
||||
assertEquals("1296338.0", convert("1296.338", "144"));
|
||||
assertEquals("10368100.0", convert("10368.100", "144"));
|
||||
assertEquals("24048100.0", convert("24048.100", "144"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retainsSubKhzPrecision() {
|
||||
assertEquals("144205.2", convert("144.205.2", "432"));
|
||||
assertEquals("1296338.25", convert("1296.338.25", "144"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsCompactLegacyFormats() {
|
||||
assertEquals("432088.0", convert("432088", "144"));
|
||||
assertEquals("10368100.0", convert("10368100", "144"));
|
||||
assertEquals("432088.2", convert("432088.2", "144"));
|
||||
assertEquals("432088.0", convert("432 088", "144"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void appliesFallbackOnlyToRelativeFrequencies() {
|
||||
assertEquals("432205.0", convert(".205", "432"));
|
||||
assertEquals("144205.0", convert("205", "144"));
|
||||
assertEquals("10368300.0", convert(".300", "10368"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingOrUnsupportedValues() {
|
||||
assertEquals("", convert(null, "144"));
|
||||
assertEquals("", convert("", "144"));
|
||||
assertEquals("", convert("not-a-frequency", "144"));
|
||||
assertEquals("", convert("599", "144"));
|
||||
assertEquals("", convert(".205", null));
|
||||
assertEquals("", convert(".205", "invalid"));
|
||||
}
|
||||
|
||||
private String convert(String frequency, String fallbackPrefix) {
|
||||
SimpleValue<String> fallback = fallbackPrefix == null
|
||||
? null
|
||||
: new SimpleValue<>(fallbackPrefix);
|
||||
|
||||
return Utils4KST.normalizeFrequencyString(
|
||||
frequency,
|
||||
fallback
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.controller.Utils4KST;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class testTimeGeneration {
|
||||
|
||||
@Test
|
||||
public static void main(String[] args) {
|
||||
|
||||
System.out.println(new Utils4KST().time_convertEpochToReadable("1664669836"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user