the browser-facing portion of osu!
at master 17 kB view raw
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\Models; 9 10use App\Models\DailyChallengeUserStats; 11use App\Models\Multiplayer\PlaylistItem; 12use App\Models\Multiplayer\Room; 13use App\Models\Multiplayer\ScoreLink; 14use App\Models\Multiplayer\UserScoreAggregate; 15use App\Models\User; 16use Carbon\CarbonImmutable; 17use Tests\TestCase; 18 19class DailyChallengeUserStatsTest extends TestCase 20{ 21 protected static function roomAddPlay(User $user, PlaylistItem $playlistItem, array $scoreParams = []): ScoreLink 22 { 23 $room = $playlistItem->room; 24 $origEndsAt = $room->ends_at; 25 $room->update(['ends_at' => CarbonImmutable::now()->addDays(1)]); 26 try { 27 return parent::roomAddPlay($user, $playlistItem, ['passed' => true, ...$scoreParams]); 28 } finally { 29 $room->update(['ends_at' => $origEndsAt]); 30 } 31 } 32 33 private static function preparePlaylistItem(CarbonImmutable $playTime): PlaylistItem 34 { 35 return PlaylistItem::factory()->create([ 36 'room_id' => Room::factory()->create([ 37 'category' => 'daily_challenge', 38 'starts_at' => $playTime->startOfDay(), 39 'ends_at' => $playTime->endOfDay(), 40 ]), 41 ]); 42 } 43 44 private static function startOfWeek(): CarbonImmutable 45 { 46 return DailyChallengeUserStats::startOfWeek(CarbonImmutable::now()->subWeeks(1)); 47 } 48 49 public function testCalculateFromStart(): void 50 { 51 $playTime = static::startOfWeek(); 52 $playlistItem = static::preparePlaylistItem($playTime); 53 54 $user = User::factory()->create(); 55 ScoreLink::factory()->passed()->create([ 56 'playlist_item_id' => $playlistItem, 57 'user_id' => $user, 58 ]); 59 UserScoreAggregate::new($user, $playlistItem->room)->save(); 60 61 $this->expectCountChange(fn () => DailyChallengeUserStats::count(), 1); 62 63 DailyChallengeUserStats::calculate($playTime); 64 65 $stats = DailyChallengeUserStats::find($user->getKey()); 66 $this->assertSame(1, $stats->playcount); 67 $this->assertSame(1, $stats->daily_streak_current); 68 $this->assertSame(1, $stats->daily_streak_best); 69 $this->assertSame(1, $stats->weekly_streak_current); 70 $this->assertSame(1, $stats->weekly_streak_best); 71 $this->assertSame(1, $stats->top_10p_placements); 72 $this->assertSame(1, $stats->top_50p_placements); 73 $this->assertTrue($playTime->equalTo($stats->last_weekly_streak)); 74 $this->assertTrue($playTime->equalTo($stats->last_update)); 75 } 76 77 public function testCalculateNoPlaysBreaksDailyStreak(): void 78 { 79 $playTime = static::startOfWeek(); 80 static::preparePlaylistItem($playTime); 81 82 $user = User::factory()->create(); 83 84 $lastWeeklyStreak = $playTime->subWeeks(1); 85 DailyChallengeUserStats::create([ 86 'daily_streak_best' => 3, 87 'daily_streak_current' => 3, 88 'last_update' => $playTime->subDays(1), 89 'last_weekly_streak' => $lastWeeklyStreak, 90 'user_id' => $user->getKey(), 91 'weekly_streak_best' => 3, 92 'weekly_streak_current' => 3, 93 ]); 94 DailyChallengeUserStats::calculate($playTime); 95 96 $stats = DailyChallengeUserStats::find($user->getKey()); 97 $this->assertSame(0, $stats->daily_streak_current); 98 $this->assertSame(3, $stats->daily_streak_best); 99 $this->assertSame(3, $stats->weekly_streak_current); 100 $this->assertSame(3, $stats->weekly_streak_best); 101 $this->assertTrue($lastWeeklyStreak->equalTo($stats->last_weekly_streak)); 102 } 103 104 public function testCalculateNoPlaysOverAWeekBreaksWeeklyStreak(): void 105 { 106 $playTime = static::startOfWeek(); 107 $playlistItem = static::preparePlaylistItem($playTime); 108 109 $user = User::factory()->create(); 110 111 $lastWeeklyStreak = $playTime->subWeeks(2); 112 DailyChallengeUserStats::create([ 113 'user_id' => $user->getKey(), 114 'weekly_streak_current' => 3, 115 'weekly_streak_best' => 3, 116 'last_update' => $playTime->subDays(1), 117 'last_weekly_streak' => $lastWeeklyStreak, 118 ]); 119 DailyChallengeUserStats::calculate($playTime); 120 121 $stats = DailyChallengeUserStats::find($user->getKey()); 122 $this->assertSame(0, $stats->weekly_streak_current); 123 $this->assertSame(3, $stats->weekly_streak_best); 124 $this->assertTrue($lastWeeklyStreak->equalTo($stats->last_weekly_streak)); 125 } 126 127 public function testCalculateNoPlaysOverAWeekBreaksWeeklyStreakLastStreakOnStartOfWeek(): void 128 { 129 $user = User::factory()->create(); 130 $lastWeeklyStreak = static::startOfWeek(); 131 DailyChallengeUserStats::create([ 132 'user_id' => $user->getKey(), 133 'weekly_streak_current' => 3, 134 'weekly_streak_best' => 3, 135 'last_update' => $lastWeeklyStreak->addDays(1), 136 'last_weekly_streak' => $lastWeeklyStreak, 137 ]); 138 139 // no break until the exact 14th day after last weekly streak 140 for ($i = 7; $i <= 14; $i++) { 141 $playTime = $lastWeeklyStreak->addDays($i); 142 $playlistItem = static::preparePlaylistItem($playTime); 143 DailyChallengeUserStats::calculate($playTime); 144 145 $stats = DailyChallengeUserStats::find($user->getKey()); 146 $testHint = "After {$i} days"; 147 $this->assertSame($i === 14 ? 0 : 3, $stats->weekly_streak_current, $testHint); 148 $this->assertSame(3, $stats->weekly_streak_best, $testHint); 149 $this->assertTrue($lastWeeklyStreak->equalTo($stats->last_weekly_streak), $testHint); 150 } 151 } 152 153 public function testCalculatePercentile(): void 154 { 155 $playTime = static::startOfWeek(); 156 $playlistItem = static::preparePlaylistItem($playTime); 157 158 $totalScores = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; 159 $scoreLinks = []; 160 foreach ($totalScores as $totalScore) { 161 $scoreLink = $scoreLinks[] = ScoreLink::factory()->completed([ 162 'passed' => true, 163 'total_score' => $totalScore, 164 ])->create([ 165 'playlist_item_id' => $playlistItem, 166 ]); 167 UserScoreAggregate::new($scoreLink->user, $playlistItem->room)->save(); 168 } 169 170 $this->expectCountChange(fn () => DailyChallengeUserStats::count(), 10); 171 172 DailyChallengeUserStats::calculate($playTime); 173 174 foreach ($scoreLinks as $i => $scoreLink) { 175 [$count10p, $count50p] = match (true) { 176 // 100 177 $i === 9 => [1, 1], 178 // 60 - 90 179 $i >= 5 => [0, 1], 180 default => [0, 0], 181 }; 182 $stats = DailyChallengeUserStats::find($scoreLink->user_id); 183 $this->assertSame($count10p, $stats->top_10p_placements, "i: {$i}"); 184 $this->assertSame($count50p, $stats->top_50p_placements, "i: {$i}"); 185 } 186 } 187 188 public function testFix(): void 189 { 190 $user = User::factory()->create(); 191 192 foreach ([14, 13, 12, 11, 10, 9, 7, 6, 5] as $subDay) { 193 $playTime = static::startOfWeek()->subDays($subDay); 194 $playlistItem = static::preparePlaylistItem($playTime); 195 $this->roomAddPlay($user, $playlistItem); 196 DailyChallengeUserStats::calculate($playTime); 197 } 198 $this->travelTo($playTime->addDays(1)); 199 200 $stats = DailyChallengeUserStats::find($user->getKey()); 201 $expectedAttributes = $stats->getAttributes(); 202 $stats->fill([...DailyChallengeUserStats::INITIAL_VALUES])->saveOrExplode(); 203 $stats->fresh()->fix(); 204 205 $stats->refresh(); 206 $this->assertSame(9, $stats->playcount); 207 $this->assertSame(3, $stats->daily_streak_current); 208 $this->assertSame(6, $stats->daily_streak_best); 209 $this->assertSame(2, $stats->weekly_streak_current); 210 $this->assertSame(2, $stats->weekly_streak_best); 211 $this->assertSame(9, $stats->top_10p_placements); 212 $this->assertSame(9, $stats->top_50p_placements); 213 214 $this->travelBack(); 215 216 $stats->fresh()->fix(); 217 218 $stats->refresh(); 219 $this->assertSame(9, $stats->playcount); 220 $this->assertSame(0, $stats->daily_streak_current); 221 $this->assertSame(6, $stats->daily_streak_best); 222 } 223 224 public function testFixZeroTotalScore(): void 225 { 226 $user = User::factory()->create(); 227 228 foreach ([3 => 100, 2 => 0, 1 => 100] as $subDay => $score) { 229 $playTime = static::startOfWeek()->subDays($subDay); 230 $playlistItem = static::preparePlaylistItem($playTime); 231 $this->roomAddPlay($user, $playlistItem, ['total_score' => $score]); 232 } 233 $this->travelTo($playTime->addDays(1)); 234 235 $stats = DailyChallengeUserStats::find($user->getKey()); 236 $stats->fill([...DailyChallengeUserStats::INITIAL_VALUES])->saveOrExplode(); 237 $stats->fix(); 238 239 $stats->refresh(); 240 $this->assertSame(2, $stats->playcount); 241 $this->assertSame(1, $stats->daily_streak_current); 242 $this->assertSame(1, $stats->daily_streak_best); 243 $this->assertSame(1, $stats->weekly_streak_current); 244 $this->assertSame(1, $stats->weekly_streak_best); 245 } 246 247 public function testFlowFromStart(): void 248 { 249 $playTime = static::startOfWeek(); 250 $playlistItem = static::preparePlaylistItem($playTime); 251 $user = User::factory()->create(); 252 253 $this->expectCountChange(fn () => DailyChallengeUserStats::count(), 1); 254 255 $this->roomAddPlay($user, $playlistItem); 256 257 $stats = DailyChallengeUserStats::find($user->getKey()); 258 $this->assertSame(1, $stats->playcount); 259 $this->assertSame(1, $stats->daily_streak_current); 260 $this->assertSame(1, $stats->daily_streak_best); 261 $this->assertSame(1, $stats->weekly_streak_current); 262 $this->assertSame(1, $stats->weekly_streak_best); 263 $this->assertSame(0, $stats->top_10p_placements); 264 $this->assertSame(0, $stats->top_50p_placements); 265 $this->assertTrue($playTime->equalTo($stats->last_weekly_streak)); 266 $this->assertTrue($playTime->equalTo($stats->last_update)); 267 268 // increments percentile and nothing else 269 DailyChallengeUserStats::calculate($playTime); 270 271 $stats->refresh(); 272 $this->assertSame(1, $stats->playcount); 273 $this->assertSame(1, $stats->daily_streak_current); 274 $this->assertSame(1, $stats->daily_streak_best); 275 $this->assertSame(1, $stats->weekly_streak_current); 276 $this->assertSame(1, $stats->weekly_streak_best); 277 $this->assertSame(1, $stats->top_10p_placements); 278 $this->assertSame(1, $stats->top_50p_placements); 279 $this->assertTrue($playTime->equalTo($stats->last_weekly_streak)); 280 $this->assertTrue($playTime->equalTo($stats->last_update)); 281 } 282 283 public function testFlowMultipleTimes(): void 284 { 285 $playTime = static::startOfWeek(); 286 $playlistItem = static::preparePlaylistItem($playTime); 287 $user = User::factory()->create(); 288 289 $this->roomAddPlay($user, $playlistItem); 290 $this->roomAddPlay($user, $playlistItem); 291 292 DailyChallengeUserStats::calculate($playTime); 293 DailyChallengeUserStats::calculate($playTime); 294 295 $stats = DailyChallengeUserStats::find($user->getKey()); 296 $this->assertSame(1, $stats->playcount); 297 } 298 299 public function testFlowIncrementAll(): void 300 { 301 $playTime = static::startOfWeek(); 302 $playlistItem = static::preparePlaylistItem($playTime); 303 $user = User::factory()->create(); 304 305 DailyChallengeUserStats::create([ 306 'user_id' => $user->getKey(), 307 'playcount' => 1, 308 'daily_streak_current' => 1, 309 'daily_streak_best' => 1, 310 'weekly_streak_current' => 1, 311 'weekly_streak_best' => 1, 312 'top_10p_placements' => 1, 313 'top_50p_placements' => 1, 314 'last_weekly_streak' => $playTime->subWeeks(1), 315 'last_update' => $playTime->subDays(1), 316 ]); 317 318 $this->expectCountChange(fn () => DailyChallengeUserStats::count(), 0); 319 320 $this->roomAddPlay($user, $playlistItem); 321 $stats = DailyChallengeUserStats::find($user->getKey()); 322 $this->assertSame(2, $stats->daily_streak_current); 323 $this->assertSame(2, $stats->daily_streak_best); 324 $this->assertSame(2, $stats->weekly_streak_current); 325 $this->assertSame(2, $stats->weekly_streak_best); 326 $this->assertSame(1, $stats->top_10p_placements); 327 $this->assertSame(1, $stats->top_50p_placements); 328 $this->assertTrue($playTime->equalTo($stats->last_weekly_streak)); 329 $this->assertTrue($playTime->equalTo($stats->last_update)); 330 331 DailyChallengeUserStats::calculate($playTime); 332 333 $stats->refresh(); 334 $this->assertSame(2, $stats->daily_streak_current); 335 $this->assertSame(2, $stats->daily_streak_best); 336 $this->assertSame(2, $stats->weekly_streak_current); 337 $this->assertSame(2, $stats->weekly_streak_best); 338 $this->assertSame(2, $stats->top_10p_placements); 339 $this->assertSame(2, $stats->top_50p_placements); 340 $this->assertTrue($playTime->equalTo($stats->last_weekly_streak)); 341 $this->assertTrue($playTime->equalTo($stats->last_update)); 342 } 343 344 public function testFlowIncrementWeeklyStreak(): void 345 { 346 $playTime = static::startOfWeek(); 347 $playlistItem = static::preparePlaylistItem($playTime); 348 $user = User::factory()->create(); 349 350 DailyChallengeUserStats::create([ 351 'user_id' => $user->getKey(), 352 'weekly_streak_current' => 1, 353 'weekly_streak_best' => 1, 354 'last_weekly_streak' => $playTime->subWeeks(1), 355 'last_update' => $playTime->subDays(1), 356 ]); 357 358 $this->roomAddPlay($user, $playlistItem); 359 360 $stats = DailyChallengeUserStats::find($user->getKey()); 361 $this->assertSame(2, $stats->weekly_streak_current); 362 $this->assertSame(2, $stats->weekly_streak_best); 363 $this->assertTrue($playTime->equalTo($stats->last_weekly_streak)); 364 365 DailyChallengeUserStats::calculate($playTime); 366 367 $stats->refresh(); 368 $this->assertSame(2, $stats->weekly_streak_current); 369 $this->assertSame(2, $stats->weekly_streak_best); 370 $this->assertTrue($playTime->equalTo($stats->last_weekly_streak)); 371 } 372 373 /** 374 * 1. normal play and calculation (day 1) 375 * 2. no play (day 2) 376 * 3. play (day 3) 377 * 4. calculation (day 2) 378 * 5. play (day 3) 379 * 6. calculation (day 3) 380 * Streak should be broken on #3 and restarted streak should stay the same on #4, #5, and #6. 381 */ 382 public function testFlowOneSkippedDayAndOnePlayBeforeLastCalculation(): void 383 { 384 $playTime = static::startOfWeek(); 385 $playlistItem = static::preparePlaylistItem($playTime); 386 387 $user = User::factory()->create(); 388 $this->roomAddPlay($user, $playlistItem, ['passed' => true]); 389 DailyChallengeUserStats::calculate($playTime); 390 391 $playTime = $playTime->addDays(1); 392 $playlistItem = static::preparePlaylistItem($playTime); 393 394 $playTime = $playTime->addDays(1); 395 $playlistItem = static::preparePlaylistItem($playTime); 396 $this->roomAddPlay($user, $playlistItem, ['passed' => true]); 397 398 // no changes expected 399 $assertValues = function () use ($user): void { 400 $stats = DailyChallengeUserStats::find($user->getKey()); 401 $this->assertSame(1, $stats->daily_streak_current); 402 $this->assertSame(1, $stats->daily_streak_best); 403 $this->assertSame(1, $stats->weekly_streak_current); 404 $this->assertSame(1, $stats->weekly_streak_best); 405 }; 406 $assertValues(); 407 408 DailyChallengeUserStats::calculate($playTime->subDays(1)); 409 $assertValues(); 410 411 $this->roomAddPlay($user, $playlistItem, ['passed' => true]); 412 $assertValues(); 413 414 DailyChallengeUserStats::calculate($playTime); 415 $assertValues(); 416 } 417 418 protected function setUp(): void 419 { 420 parent::setUp(); 421 // prevent storing percentile cache 422 config_set('cache.default', 'array'); 423 } 424}