1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6declare(strict_types=1);
7
8namespace Tests\Commands;
9
10use App\Console\Commands\ModdingRankCommand;
11use App\Enums\Ruleset;
12use App\Jobs\CheckBeatmapsetCovers;
13use App\Jobs\Notifications\BeatmapsetRank;
14use App\Models\Beatmap;
15use App\Models\BeatmapDiscussion;
16use App\Models\Beatmapset;
17use Bus;
18use Database\Factories\BeatmapsetFactory;
19use Tests\TestCase;
20
21class ModdingRankCommandTest extends TestCase
22{
23 public function testCountOnly(): void
24 {
25 $this->beatmapset([Ruleset::osu])->create();
26
27 $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 0);
28
29 $this->artisan('modding:rank', ['--count-only' => true]);
30
31 Bus::assertNotDispatched(CheckBeatmapsetCovers::class);
32 Bus::assertNotDispatched(BeatmapsetRank::class);
33 }
34
35 /**
36 * @dataProvider rankDataProvider
37 */
38 public function testRank(int $qualifiedDaysAgo, int $expected): void
39 {
40 $this->beatmapset([Ruleset::osu], $qualifiedDaysAgo)->create();
41
42 $this->expectCountChange(fn () => Beatmapset::ranked()->count(), $expected);
43
44 $this->artisan('modding:rank', ['--no-wait' => true]);
45
46 Bus::assertDispatchedTimes(CheckBeatmapsetCovers::class, $expected);
47 Bus::assertDispatchedTimes(BeatmapsetRank::class, $expected);
48 }
49
50 /**
51 * @dataProvider rankHybridDataProvider
52 */
53 public function testRankHybrid(array $beatmapsetRulesets, array $expectedCounts): void
54 {
55 foreach ($beatmapsetRulesets as $rulesets) {
56 $this->beatmapset($rulesets)->create();
57 }
58
59 foreach (Ruleset::cases() as $ruleset) {
60 $this->assertSame($expectedCounts[$ruleset->value], ModdingRankCommand::getStats($ruleset)['inQueue']);
61 }
62 }
63
64 public function testRankOpenIssue(): void
65 {
66 $this->beatmapset([Ruleset::osu])
67 ->has(BeatmapDiscussion::factory()->general()->problem())
68 ->create();
69
70 $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 0);
71
72 $this->artisan('modding:rank', ['--no-wait' => true]);
73
74 Bus::assertNotDispatched(CheckBeatmapsetCovers::class);
75 Bus::assertNotDispatched(BeatmapsetRank::class);
76 }
77
78 public function testRankQuota(): void
79 {
80 $this->beatmapset([Ruleset::osu])->count(3)->create();
81
82 $this->expectCountChange(fn () => Beatmapset::qualified()->count(), -2);
83 $this->expectCountChange(fn () => Beatmapset::ranked()->count(), 2);
84
85 $this->artisan('modding:rank', ['--no-wait' => true]);
86
87 Bus::assertDispatched(CheckBeatmapsetCovers::class);
88 Bus::assertDispatched(BeatmapsetRank::class);
89 }
90
91 public function testRankQuotaSeparateRuleset(): void
92 {
93 foreach (Ruleset::cases() as $ruleset) {
94 $this->beatmapset([$ruleset])->create();
95 }
96
97 $count = count(Ruleset::cases());
98 $this->expectCountChange(fn () => Beatmapset::ranked()->count(), $count);
99
100 $this->artisan('modding:rank', ['--no-wait' => true]);
101
102 Bus::assertDispatchedTimes(CheckBeatmapsetCovers::class, $count);
103 Bus::assertDispatchedTimes(BeatmapsetRank::class, $count);
104 }
105
106
107 public static function rankDataProvider()
108 {
109 // 1 day ago isn't used because it might or might not be equal to the cutoff depending on how fast it runs.
110 return [
111 [0, 0],
112 [2, 1],
113 ];
114 }
115
116 public static function rankHybridDataProvider()
117 {
118 return [
119 // hybrid counts as ruleset with lowest enum value
120 [[[Ruleset::osu, Ruleset::taiko, Ruleset::catch, Ruleset::mania]], [1, 0, 0, 0]],
121 [[[Ruleset::taiko, Ruleset::catch, Ruleset::mania]], [0, 1, 0, 0]],
122 [[[Ruleset::catch, Ruleset::mania]], [0, 0, 1, 0]],
123 [[[Ruleset::mania]], [0, 0, 0, 1]],
124
125 // not comprehensive
126 [[[Ruleset::osu, Ruleset::taiko], [Ruleset::osu]], [2, 0, 0, 0]],
127 [[[Ruleset::osu, Ruleset::taiko], [Ruleset::taiko]], [1, 1, 0, 0]],
128 [[[Ruleset::mania, Ruleset::taiko], [Ruleset::taiko]], [0, 2, 0, 0]],
129 [[[Ruleset::mania, Ruleset::taiko], [Ruleset::mania]], [0, 1, 0, 1]],
130 [[[Ruleset::catch, Ruleset::taiko], [Ruleset::mania]], [0, 1, 0, 1]],
131 ];
132 }
133
134 protected function setUp(): void
135 {
136 parent::setUp();
137
138 config_set('osu.beatmapset.minimum_days_for_rank', 1);
139 config_set('osu.beatmapset.rank_per_day', 2);
140
141 Bus::fake([BeatmapsetRank::class, CheckBeatmapsetCovers::class]);
142 }
143
144 /**
145 * @param Ruleset[] $rulesets
146 */
147 protected function beatmapset(array $rulesets, int $qualifiedDaysAgo = 2): BeatmapsetFactory
148 {
149 $factory = Beatmapset::factory()
150 ->owner()
151 ->qualified(now()->subDays($qualifiedDaysAgo));
152
153 foreach ($rulesets as $ruleset) {
154 $factory = $factory->has(Beatmap::factory()->ruleset($ruleset->legacyName()));
155 }
156
157 return $factory;
158 }
159}