the browser-facing portion of osu!
at master 8.9 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 6namespace Tests\Libraries\Search; 7 8use App\Libraries\Search\BeatmapsetQueryParser; 9use App\Models\Beatmapset; 10use Carbon\CarbonImmutable; 11use Tests\TestCase; 12 13class BeatmapsetQueryParserTest extends TestCase 14{ 15 public static function queryDataProvider() 16 { 17 return [ 18 // basic options 19 ['stars=1', ['keywords' => null, 'options' => ['stars' => ['gte' => 0.995, 'lte' => 1.005]]]], 20 ['star=1', ['keywords' => null, 'options' => ['stars' => ['gte' => 0.995, 'lte' => 1.005]]]], 21 ['ar=2', ['keywords' => null, 'options' => ['ar' => ['gte' => 1.95, 'lte' => 2.05]]]], 22 ['dr=3', ['keywords' => null, 'options' => ['dr' => ['gte' => 2.95, 'lte' => 3.05]]]], 23 ['hp<4', ['keywords' => null, 'options' => ['dr' => ['lte' => 3.95]]]], 24 ['cs>5', ['keywords' => null, 'options' => ['cs' => ['gte' => 5.05]]]], 25 ['od>=9', ['keywords' => null, 'options' => ['od' => ['gte' => 8.95]]]], 26 ['bpm<=6', ['keywords' => null, 'options' => ['bpm' => ['lte' => 6.005]]]], 27 ['length<70000ms', ['keywords' => null, 'options' => ['length' => ['lte' => 69.9995]]]], 28 ['length>=70', ['keywords' => null, 'options' => ['length' => ['gte' => 69.5]]]], 29 ['length>=70s', ['keywords' => null, 'options' => ['length' => ['gte' => 69.5]]]], 30 ['length:8m', ['keywords' => null, 'options' => ['length' => ['gte' => 450, 'lte' => 510]]]], 31 ['length:0.9h', ['keywords' => null, 'options' => ['length' => ['gte' => (0.9 * 3600 - 1800), 'lte' => (0.9 * 3600 + 1800)]]]], 32 ['keys=10', ['keywords' => null, 'options' => ['keys' => ['gte' => 10, 'lte' => 10]]]], 33 ['divisor>0', ['keywords' => null, 'options' => ['divisor' => ['gt' => 0]]]], 34 ['status<ranked', ['keywords' => null, 'options' => ['status' => ['lt' => Beatmapset::STATES['ranked']]]]], 35 ['status=graveyard', ['keywords' => null, 'options' => ['status' => ['gte' => Beatmapset::STATES['graveyard'], 'lte' => Beatmapset::STATES['graveyard']]]]], 36 ['creator=hello', ['keywords' => null, 'options' => ['creator' => 'hello']]], 37 ['artist=hello', ['keywords' => null, 'options' => ['artist' => 'hello']]], 38 ['artist="hello world"', ['keywords' => null, 'options' => ['artist' => 'hello world']]], 39 ['created=2017', ['keywords' => null, 'options' => ['created' => ['gte' => static::parseTime('2017-01-01'), 'lt' => static::parseTime('2018-01-01')]]]], 40 ['ranked>2018', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2019-01-01')]]]], 41 ['ranked<2018-05', ['keywords' => null, 'options' => ['ranked' => ['lt' => static::parseTime('2018-05-01')]]]], 42 ['ranked<=2018.05', ['keywords' => null, 'options' => ['ranked' => ['lt' => static::parseTime('2018-06-01')]]]], 43 ['ranked=2018/05', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2018-05-01'), 'lt' => static::parseTime('2018-06-01')]]]], 44 ['ranked=2018.05.01', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2018-05-01'), 'lt' => static::parseTime('2018-05-02')]]]], 45 ['ranked>2018/05/01', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2018-05-02')]]]], 46 ['ranked>="2020-07-21 12:30:30 +09:00"', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2020-07-21 03:30:30')]]]], 47 ['ranked="2020-07-21 12:30:30 +09:00"', ['keywords' => null, 'options' => ['ranked' => ['gte' => static::parseTime('2020-07-21 03:30:30'), 'lt' => static::parseTime('2020-07-21 03:30:31')]]]], 48 ['ranked="invalid date format"', ['keywords' => 'ranked="invalid date format"', 'options' => []]], 49 50 // multiple options 51 ['artist=hello creator:world', ['keywords' => null, 'options' => ['artist' => 'hello', 'creator' => 'world']]], 52 53 // last option overrides previous ones 54 ['dr=1 dr=9', ['keywords' => null, 'options' => ['dr' => ['gte' => 8.95, 'lte' => 9.05]]]], 55 ['artist=hello artist:world', ['keywords' => null, 'options' => ['artist' => 'world']]], 56 57 // last option overrides previous ones, including with different names 58 ['dr=1 hp<9', ['keywords' => null, 'options' => ['dr' => ['gte' => 0.95, 'lte' => 8.95]]]], 59 60 // keyword with options 61 ['hello stars>=1 stars<4', ['keywords' => 'hello', 'options' => ['stars' => ['gte' => 0.995, 'lte' => 3.995]]]], 62 63 // keywords with option in between 64 ['hello ar<:1 world', ['keywords' => 'hello world', 'options' => ['ar' => ['lte' => 1.05]]]], 65 66 // option with invalid operator is ignored (and becomes keyword) 67 ['artist>a', ['keywords' => 'artist>a', 'options' => []]], 68 ['dr=a', ['keywords' => 'dr=a', 'options' => []]], 69 70 // taken from https://github.com/ppy/osu/blob/b3e96c8385fdfec3ea1bb3899f74763ccafa055c/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs 71 ['stars<4 easy', ['keywords' => 'easy', 'options' => ['stars' => ['lte' => 3.995]]]], 72 ['ar>=9 difficult', ['keywords' => 'difficult', 'options' => ['ar' => ['gte' => 8.95]]]], 73 ['dr>2 quite specific dr<:6', ['keywords' => 'quite specific', 'options' => ['dr' => ['gte' => 2.05, 'lte' => 6.05]]]], 74 ['hp>2 quite specific hp<=6', ['keywords' => 'quite specific', 'options' => ['dr' => ['gte' => 2.05, 'lte' => 6.05]]]], 75 ['od>4 easy od<8', ['keywords' => 'easy', 'options' => ['od' => ['gte' => 4.05, 'lte' => 7.95]]]], 76 ['bpm>:200 gotta go fast', ['keywords' => 'gotta go fast', 'options' => ['bpm' => ['gte' => 199.995]]]], 77 ['length=6ms time', ['keywords' => 'time', 'options' => ['length' => ['gte' => (6 / 1000 - 1 / 2000), 'lte' => (6 / 1000 + 1 / 2000)]]]], 78 ['length=23s time', ['keywords' => 'time', 'options' => ['length' => ['gte' => 22.5, 'lte' => 23.5]]]], 79 ['length=9m time', ['keywords' => 'time', 'options' => ['length' => ['gte' => (9 * 60 - 30), 'lte' => (9 * 60 + 30)]]]], 80 ['length=0.25h time', ['keywords' => 'time', 'options' => ['length' => ['gte' => (0.25 * 3600 - 1800), 'lte' => (0.25 * 3600 + 1800)]]]], 81 ['length=70 time', ['keywords' => 'time', 'options' => ['length' => ['gte' => 69.5, 'lte' => 70.5]]]], 82 ["that's a time signature alright! divisor:12", ['keywords' => "that's a time signature alright!", 'options' => ['divisor' => ['gte' => 12, 'lte' => 12]]]], 83 ['I want the pp status=ranked', ['keywords' => 'I want the pp', 'options' => ['status' => ['gte' => Beatmapset::STATES['ranked'], 'lte' => Beatmapset::STATES['ranked']]]]], 84 ['beatmap specifically by creator=my_fav', ['keywords' => 'beatmap specifically by', 'options' => ['creator' => 'my_fav']]], 85 ['find me songs by artist=singer please', ['keywords' => 'find me songs by please', 'options' => ['artist' => 'singer']]], 86 ['really like artist="name with space" yes', ['keywords' => 'really like yes', 'options' => ['artist' => 'name with space']]], 87 ['weird artist=double"quote', ['keywords' => 'weird', 'options' => ['artist' => 'double"quote']]], 88 ['weird artist="nested "quote"" thing', ['keywords' => 'weird thing', 'options' => ['artist' => 'nested "quote"']]], 89 ['artist=><something', ['keywords' => null, 'options' => ['artist' => '><something']]], 90 ['unrecognised=keyword', ['keywords' => 'unrecognised=keyword', 'options' => []]], 91 ['cs=nope', ['keywords' => 'cs=nope', 'options' => []]], 92 ['bpm=bad', ['keywords' => 'bpm=bad', 'options' => []]], 93 ['divisor<nah', ['keywords' => 'divisor<nah', 'options' => []]], 94 ['status=noidea', ['keywords' => 'status=noidea', 'options' => []]], 95 ['status=l', ['keywords' => null, 'options' => ['status' => ['gte' => Beatmapset::STATES['loved'], 'lte' => Beatmapset::STATES['loved']]]]], 96 ['status=lo', ['keywords' => null, 'options' => ['status' => ['gte' => Beatmapset::STATES['loved'], 'lte' => Beatmapset::STATES['loved']]]]], 97 ]; 98 } 99 100 private static function parseTime(string $timeString): int 101 { 102 return CarbonImmutable::parse($timeString)->getTimestampMs(); 103 } 104 105 /** 106 * @dataProvider queryDataProvider 107 */ 108 public function testParse(?string $query, ?array $expected) 109 { 110 $this->assertSame(json_encode($expected), json_encode(BeatmapsetQueryParser::parse($query))); 111 } 112}