'145 MHz'], [ 'order' => 1, 'edi_band_begin' => 144.0, 'edi_band_end' => 146.0, 'has_power_category' => true, ] ); $category = Category::firstOrCreate( ['name' => 'SINGLE'], [ 'order' => 1, ] ); $power = PowerCategory::firstOrCreate( ['name' => 'LP'], [ 'order' => 1, 'power_level' => 100, ] ); $ruleSetUnique = EvaluationRuleSet::firstOrCreate( ['code' => 'REG_UNIQUE'], [ 'name' => 'Regression UNIQUE', 'scoring_mode' => 'FIXED_POINTS', 'points_per_qso' => 1, 'use_multipliers' => false, 'dup_qso_policy' => 'ZERO_POINTS', 'nil_qso_policy' => 'ZERO_POINTS', 'busted_call_policy' => 'ZERO_POINTS', 'busted_exchange_policy' => 'ZERO_POINTS', 'time_tolerance_sec' => 60, 'require_unique_qso' => true, ] ); $ruleSetNoUnique = EvaluationRuleSet::firstOrCreate( ['code' => 'REG_DUP'], [ 'name' => 'Regression DUP', 'scoring_mode' => 'FIXED_POINTS', 'points_per_qso' => 1, 'use_multipliers' => false, 'dup_qso_policy' => 'ZERO_POINTS', 'nil_qso_policy' => 'ZERO_POINTS', 'busted_call_policy' => 'ZERO_POINTS', 'busted_exchange_policy' => 'ZERO_POINTS', 'time_tolerance_sec' => 60, 'require_unique_qso' => false, ] ); $ruleSetTimeMismatch = EvaluationRuleSet::firstOrCreate( ['code' => 'REG_TIME_MISMATCH'], [ 'name' => 'Regression TIME_MISMATCH', 'scoring_mode' => 'FIXED_POINTS', 'points_per_qso' => 1, 'use_multipliers' => false, 'dup_qso_policy' => 'ZERO_POINTS', 'nil_qso_policy' => 'ZERO_POINTS', 'busted_call_policy' => 'ZERO_POINTS', 'busted_exchange_policy' => 'ZERO_POINTS', 'time_tolerance_sec' => 60, 'allow_time_mismatch_pairing' => true, 'time_mismatch_max_sec' => 600, 'time_mismatch_policy' => 'ZERO_POINTS', ] ); $ruleSetFlags = EvaluationRuleSet::firstOrCreate( ['code' => 'REG_FLAGS'], [ 'name' => 'Regression FLAGS', 'scoring_mode' => 'FIXED_POINTS', 'points_per_qso' => 1, 'use_multipliers' => false, 'dup_qso_policy' => 'ZERO_POINTS', 'nil_qso_policy' => 'ZERO_POINTS', 'busted_call_policy' => 'ZERO_POINTS', 'busted_exchange_policy' => 'ZERO_POINTS', 'exchange_requires_report' => true, 'exchange_requires_serial' => true, 'rst_ignore_third_char' => true, 'ignore_slash_part' => true, 'callsign_suffix_max_len' => 4, 'callsign_levenshtein_max' => 1, 'discard_qso_rec_diff_rst' => true, 'discard_qso_rec_diff_serial' => true, 'time_tolerance_sec' => 60, ] ); $contest = Contest::create([ 'name' => ['cs' => 'Regression Contest', 'en' => 'Regression Contest'], 'description' => ['cs' => 'Regresní sada', 'en' => 'Regression set'], 'is_active' => false, 'is_test' => true, 'rule_set_id' => $ruleSetUnique->id, ]); $contest->bands()->sync([$band->id]); $contest->categories()->sync([$category->id]); $contest->powerCategories()->sync([$power->id]); $this->scenarioUniqueGlobal($contest, $band, $category, $power, $ruleSetUnique); $this->scenarioUniqueNotGlobal($contest, $band, $category, $power, $ruleSetUnique); $this->scenarioDuplicateResolution($contest, $band, $category, $power, $ruleSetNoUnique); $this->scenarioTimeMismatchPolicy($contest, $band, $category, $power, $ruleSetTimeMismatch); $this->scenarioFlagCoverage($contest, $band, $category, $power, $ruleSetFlags); } private function scenarioUniqueGlobal( Contest $contest, Band $band, Category $category, PowerCategory $power, EvaluationRuleSet $ruleSet ): void { $round = $this->makeRound($contest, $ruleSet, 'UNIQUE global'); $this->attachRoundScopes($round, $band, $category, $power); $log = $this->makeLog($round, 'OK1AAA', 'JN79AB', '145'); $qso = LogQso::create([ 'log_id' => $log->id, 'qso_index' => 1, 'time_on' => Carbon::parse($round->start_time)->addMinutes(5), 'freq_khz' => 144000, 'my_call' => 'OK1AAA', 'dx_call' => 'UNIQUE1', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); $run = $this->makeRun($round, $ruleSet); $this->runPipeline($run); $errorCode = QsoResult::where('evaluation_run_id', $run->id) ->where('log_qso_id', $qso->id) ->value('error_code'); if ($errorCode !== 'UNIQUE') { throw new RuntimeException("Regression UNIQUE global selhal: očekáván UNIQUE, got {$errorCode}."); } } private function scenarioUniqueNotGlobal( Contest $contest, Band $band, Category $category, PowerCategory $power, EvaluationRuleSet $ruleSet ): void { $round = $this->makeRound($contest, $ruleSet, 'UNIQUE not global'); $this->attachRoundScopes($round, $band, $category, $power); $logA = $this->makeLog($round, 'OK1AAB', 'JN79AB', '145'); $logB = $this->makeLog($round, 'OK1AAC', 'JN79AB', '145'); $qsoA = LogQso::create([ 'log_id' => $logA->id, 'qso_index' => 1, 'time_on' => Carbon::parse($round->start_time)->addMinutes(5), 'freq_khz' => 144000, 'my_call' => 'OK1AAB', 'dx_call' => 'X2AAA', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); LogQso::create([ 'log_id' => $logB->id, 'qso_index' => 1, 'time_on' => Carbon::parse($round->start_time)->addMinutes(6), 'freq_khz' => 144000, 'my_call' => 'OK1AAC', 'dx_call' => 'X2AAA', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); $run = $this->makeRun($round, $ruleSet); $this->runPipeline($run); $errorCode = QsoResult::where('evaluation_run_id', $run->id) ->where('log_qso_id', $qsoA->id) ->value('error_code'); if ($errorCode === 'UNIQUE') { throw new RuntimeException('Regression UNIQUE not global selhal: UNIQUE nesmí vzniknout.'); } } private function scenarioDuplicateResolution( Contest $contest, Band $band, Category $category, PowerCategory $power, EvaluationRuleSet $ruleSet ): void { $round = $this->makeRound($contest, $ruleSet, 'DUP resolution'); $this->attachRoundScopes($round, $band, $category, $power); $log = $this->makeLog($round, 'OK1DUP', 'JN79AB', '145'); LogQso::create([ 'log_id' => $log->id, 'qso_index' => 1, 'time_on' => Carbon::parse($round->start_time)->addMinutes(5), 'freq_khz' => 144000, 'my_call' => 'OK1DUP', 'dx_call' => 'DUP1', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); LogQso::create([ 'log_id' => $log->id, 'qso_index' => 2, 'time_on' => Carbon::parse($round->start_time)->addMinutes(6), 'freq_khz' => 144000, 'my_call' => 'OK1DUP', 'dx_call' => 'DUP1', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); $run = $this->makeRun($round, $ruleSet); $this->runPipeline($run); $dupCount = QsoResult::where('evaluation_run_id', $run->id) ->where('error_code', 'DUP') ->count(); if ($dupCount !== 1) { throw new RuntimeException("Regression DUP selhal: očekáván 1 DUP, got {$dupCount}."); } } private function scenarioTimeMismatchPolicy( Contest $contest, Band $band, Category $category, PowerCategory $power, EvaluationRuleSet $ruleSet ): void { $round = $this->makeRound($contest, $ruleSet, 'TIME_MISMATCH policy'); $this->attachRoundScopes($round, $band, $category, $power); $logA = $this->makeLog($round, 'OK1TMA', 'JN79AB', '145'); $logB = $this->makeLog($round, 'OK1TMB', 'JN79AB', '145'); $timeA = Carbon::parse($round->start_time)->addMinutes(5); $timeB = Carbon::parse($round->start_time)->addMinutes(9); $qsoA = LogQso::create([ 'log_id' => $logA->id, 'qso_index' => 1, 'time_on' => $timeA, 'freq_khz' => 144000, 'my_call' => 'OK1TMA', 'dx_call' => 'OK1TMB', 'my_serial' => '001', 'dx_serial' => '002', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); LogQso::create([ 'log_id' => $logB->id, 'qso_index' => 1, 'time_on' => $timeB, 'freq_khz' => 144000, 'my_call' => 'OK1TMB', 'dx_call' => 'OK1TMA', 'my_serial' => '002', 'dx_serial' => '001', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); $run = $this->makeRun($round, $ruleSet); $this->runPipeline($run); $result = QsoResult::where('evaluation_run_id', $run->id) ->where('log_qso_id', $qsoA->id) ->first(); if (! $result || $result->error_code !== 'TIME_MISMATCH') { throw new RuntimeException('Regression TIME_MISMATCH selhal: očekáván TIME_MISMATCH.'); } if (! $result->is_valid || (int) $result->points !== 0) { throw new RuntimeException('Regression TIME_MISMATCH selhal: očekáván validní QSO s 0 body.'); } } private function scenarioFlagCoverage( Contest $contest, Band $band, Category $category, PowerCategory $power, EvaluationRuleSet $ruleSet ): void { $round = $this->makeRound($contest, $ruleSet, 'FLAGS coverage'); $this->attachRoundScopes($round, $band, $category, $power); $logSuffix = $this->makeLog($round, 'OK1AAA/P', 'JN79AB', '145'); $qsoFuzzySource = LogQso::create([ 'log_id' => $logSuffix->id, 'qso_index' => 1, 'time_on' => Carbon::parse($round->start_time)->addMinutes(2), 'freq_khz' => 144000, 'my_call' => 'OK1AAA/P', 'dx_call' => 'OK1AAB', 'my_rst' => '599', 'dx_rst' => '599', 'my_serial' => '001', 'dx_serial' => '002', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); $logFuzzy = $this->makeLog($round, 'OK1AAB', 'JN79AB', '145'); $qsoFuzzy = LogQso::create([ 'log_id' => $logFuzzy->id, 'qso_index' => 1, 'time_on' => Carbon::parse($round->start_time)->addMinutes(2), 'freq_khz' => 144000, 'my_call' => 'OK1AAB', 'dx_call' => 'OK1AAX', 'my_rst' => '599', 'dx_rst' => '599', 'my_serial' => '002', 'dx_serial' => '001', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); $logSerial = $this->makeLog($round, 'OK1SER', 'JN79AB', '145'); $qsoSerialMismatch = LogQso::create([ 'log_id' => $logSuffix->id, 'qso_index' => 2, 'time_on' => Carbon::parse($round->start_time)->addMinutes(3), 'freq_khz' => 144000, 'my_call' => 'OK1AAA/P', 'dx_call' => 'OK1SER', 'my_rst' => '599', 'dx_rst' => '599', 'my_serial' => '005', 'dx_serial' => '006', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); LogQso::create([ 'log_id' => $logSerial->id, 'qso_index' => 1, 'time_on' => Carbon::parse($round->start_time)->addMinutes(3), 'freq_khz' => 144000, 'my_call' => 'OK1SER', 'dx_call' => 'OK1AAA', 'my_rst' => '599', 'dx_rst' => '599', 'my_serial' => '007', 'dx_serial' => '008', 'my_locator' => 'JN79AB', 'rx_wwl' => 'JN79AB', ]); $run = $this->makeRun($round, $ruleSet); $this->runPipeline($run); $suffixNorm = \App\Models\WorkingQso::where('evaluation_run_id', $run->id) ->where('log_id', $logSuffix->id) ->value('call_norm'); if ($suffixNorm !== 'OK1AAA') { throw new RuntimeException('Regression FLAGS selhal: callsign suffix nebyl normalizován.'); } $fuzzyResult = QsoResult::where('evaluation_run_id', $run->id) ->where('log_qso_id', $qsoFuzzy->id) ->first(); if (! $fuzzyResult || $fuzzyResult->match_type !== 'MATCH_FUZZY_CALL_1') { throw new RuntimeException('Regression FLAGS selhal: callsign_levenshtein_max neovlivňuje matching.'); } if ($fuzzyResult->error_code === 'BUSTED_RST') { throw new RuntimeException('Regression FLAGS selhal: rst_ignore_third_char nebylo zohledněno.'); } $serialResult = QsoResult::where('evaluation_run_id', $run->id) ->where('log_qso_id', $qsoSerialMismatch->id) ->where('error_code', 'BUSTED_SERIAL') ->first(); if (! $serialResult) { throw new RuntimeException('Regression FLAGS selhal: discard_qso_rec_diff_serial nebylo zohledněno.'); } } private function runPipeline(EvaluationRun $run): void { (new PrepareRunJob($run->id))->handle(); (new BuildWorkingSetJob($run->id))->handle(); $run = EvaluationRun::find($run->id); $group = $run && isset($run->scope['groups'][0]) ? $run->scope['groups'][0] : [ 'key' => 'b0', 'band_id' => null, 'category_id' => null, 'power_category_id' => null, ]; if (! ($group['band_id'] ?? null)) { $workingBandId = WorkingQso::where('evaluation_run_id', $run->id) ->whereNotNull('band_id') ->value('band_id'); if ($workingBandId) { $group['band_id'] = (int) $workingBandId; $group['key'] = 'b' . $workingBandId; } } $groupKey = $group['key'] ?? 'b0'; (new MatchQsoGroupJob($run->id, $groupKey, $group, 1))->handle(); (new MatchQsoGroupJob($run->id, $groupKey, $group, 2))->handle(); (new UnpairedClassificationJob($run->id))->handle(); (new DuplicateResolutionJob($run->id))->handle(); (new ScoreGroupJob($run->id, $groupKey, $group))->handle(); } private function makeRound(Contest $contest, EvaluationRuleSet $ruleSet, string $label): Round { $start = now()->subDays(1); $end = now()->subDays(1)->addHours(5); return Round::create([ 'contest_id' => $contest->id, 'rule_set_id' => $ruleSet->id, 'name' => ['cs' => "Regrese {$label}", 'en' => "Regression {$label}"], 'description' => ['cs' => 'Regresní běh', 'en' => 'Regression run'], 'is_test' => true, 'is_active' => false, 'start_time' => $start, 'end_time' => $end, 'logs_deadline' => now(), ]); } private function attachRoundScopes(Round $round, Band $band, Category $category, PowerCategory $power): void { $round->bands()->sync([$band->id]); $round->categories()->sync([$category->id]); $round->powerCategories()->sync([$power->id]); } private function makeLog(Round $round, string $pcall, string $pwwlo, string $pband): Log { return Log::create([ 'round_id' => $round->id, 'pcall' => $pcall, 'pwwlo' => $pwwlo, 'pband' => $pband, 'power_category_id' => PowerCategory::first()?->id, ]); } private function makeRun(Round $round, EvaluationRuleSet $ruleSet): EvaluationRun { return EvaluationRun::create([ 'round_id' => $round->id, 'rule_set_id' => $ruleSet->id, 'status' => 'PENDING', 'current_step' => 'seed', 'is_official' => false, 'result_type' => 'TEST', ]); } }