mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-15 14:25:33 +02:00
fix(lyrics): align usability checks with shared fixtures
Trim after removing inline timestamps before matching speaker prefixes in Dart. Run one 21-case usability corpus through Dart, Go, and Kotlin tests. Document the shared fixture and trigger Dart and Go CI when its resources change.
This commit is contained in:
@@ -9,6 +9,9 @@ gradlew text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# Empty TSV fields and trailing lyric whitespace are intentional test data.
|
||||
android/app/src/test/resources/lyrics_usability_cases.tsv whitespace=-blank-at-eol
|
||||
|
||||
# Binary files
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
@@ -32,6 +32,7 @@ jobs:
|
||||
dart:
|
||||
- 'lib/**'
|
||||
- 'test/**'
|
||||
- 'android/app/src/test/resources/**'
|
||||
- 'assets/**'
|
||||
- 'pubspec.yaml'
|
||||
- 'pubspec.lock'
|
||||
@@ -41,6 +42,7 @@ jobs:
|
||||
- '.github/workflows/ci.yml'
|
||||
go:
|
||||
- 'go_backend/**'
|
||||
- 'android/app/src/test/resources/**'
|
||||
- '.github/workflows/ci.yml'
|
||||
android:
|
||||
- 'android/**'
|
||||
|
||||
@@ -112,6 +112,10 @@ Dart should consume that declaration without knowing which extension uses it.
|
||||
Run checks that cover the code you changed. Before opening a PR, the relevant
|
||||
commands should pass.
|
||||
|
||||
Cross-language lyric usability cases live in
|
||||
`android/app/src/test/resources/lyrics_usability_cases.tsv`. Dart, Go, and
|
||||
Android tests read the same cases; add a case there when changing that policy.
|
||||
|
||||
Flutter and Dart:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -7,6 +7,29 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class NativeFinalizationPolicyTest {
|
||||
@Test
|
||||
fun matchesSharedLyricUsabilityCases() {
|
||||
val stream = checkNotNull(
|
||||
javaClass.getResourceAsStream("/lyrics_usability_cases.tsv"),
|
||||
)
|
||||
stream.bufferedReader().useLines { lines ->
|
||||
for (line in lines) {
|
||||
if (line.isBlank() || line.startsWith("#")) continue
|
||||
val fields = line.split('\t')
|
||||
assertEquals("invalid shared fixture: $line", 3, fields.size)
|
||||
val lyrics = fields[2]
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\r", "\r")
|
||||
.replace("\\t", "\t")
|
||||
assertEquals(
|
||||
fields[0],
|
||||
fields[1].toBooleanStrict(),
|
||||
NativeFinalizationPolicy.hasUsableLyricsContent(lyrics),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun usableLyricsRejectsHeadersButKeepsRealAndInstrumentalContent() {
|
||||
assertFalse(
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Shared Dart/Go/Kotlin lyric-usability cases.
|
||||
# Columns: name<TAB>expected boolean<TAB>lyrics. Decode literal \n, \r, \t in lyrics.
|
||||
empty false
|
||||
whitespace false \t\r\n
|
||||
metadata_only false [ar:Artist]\n[ti:Title]\n[offset:0]
|
||||
metadata_case false [AR:Artist]\n[BY:SpotiFLAC]
|
||||
instrumental true [Instrumental:TRUE]
|
||||
timestamps_only false [00:01.00]\n<00:01.10>
|
||||
multiple_timestamps false [00:01.00][01:02:500]
|
||||
speaker_only false v1:\n V2:
|
||||
inline_before_spaced_speaker false [00:00.00]<00:00.01> v1:
|
||||
inline_before_tabbed_speaker false [00:00.00]<00:00.01>\tV2:\t
|
||||
multiple_inline_speaker false [00:00.00]<00:00.01> <00:00.02> v2:
|
||||
empty_background false [bg: ]
|
||||
empty_timed_background false [bg:[00:00.00]<00:00.01> v1: ]
|
||||
plain_text true First line\nSecond line
|
||||
timed_text true [00:01.00]First line
|
||||
timed_speaker_text true [00:02:500]<00:02.500> v2: Harmony line
|
||||
background_text true [bg:Backing vocal]
|
||||
timed_background_text true [BG:[00:00.00]<00:00.01> v1: Backing vocal]
|
||||
multiple_timed_text true [00:01.00][00:02.00]Repeated line
|
||||
metadata_and_text true [ti:Title]\n[00:00.00]Actual lyric
|
||||
unicode_text true [00:00.00]<00:00.01> v1: 日本語の歌詞
|
||||
|
@@ -0,0 +1,35 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSharedLyricsUsabilityCases(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("..", "android", "app", "src", "test", "resources", "lyrics_usability_cases.tsv"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
decode := strings.NewReplacer(`\n`, "\n", `\r`, "\r", `\t`, "\t")
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(line, "\t")
|
||||
if len(fields) != 3 {
|
||||
t.Fatalf("invalid shared fixture: %q", line)
|
||||
}
|
||||
t.Run(fields[0], func(t *testing.T) {
|
||||
want, err := strconv.ParseBool(fields[1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := rawLyricsHasUsableContent(decode.Replace(fields[2])); got != want {
|
||||
t.Errorf("rawLyricsHasUsableContent(%q) = %v, want %v", fields[2], got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ String cleanLyricsForDisplay(String lyrics) {
|
||||
while (_lrcDisplayTimestampPattern.hasMatch(cleaned)) {
|
||||
cleaned = cleaned.replaceFirst(_lrcDisplayTimestampPattern, '').trim();
|
||||
}
|
||||
cleaned = cleaned.replaceAll(_lrcDisplayInlineTimestampPattern, '');
|
||||
cleaned = cleaned.replaceAll(_lrcDisplayInlineTimestampPattern, '').trim();
|
||||
cleaned = cleaned.replaceFirst(_lrcDisplaySpeakerPrefixPattern, '');
|
||||
cleaned = cleaned.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('shared lyric usability cases', () {
|
||||
final cases = File(
|
||||
'android/app/src/test/resources/lyrics_usability_cases.tsv',
|
||||
).readAsLinesSync();
|
||||
for (final line in cases) {
|
||||
if (line.isEmpty || line.startsWith('#')) continue;
|
||||
final fields = line.split('\t');
|
||||
test(fields.first, () {
|
||||
expect(fields, hasLength(3));
|
||||
final lyrics = fields[2]
|
||||
.replaceAll(r'\n', '\n')
|
||||
.replaceAll(r'\r', '\r')
|
||||
.replaceAll(r'\t', '\t');
|
||||
expect(hasUsableLyricsContent(lyrics), fields[1] == 'true');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group('lyrics display normalization', () {
|
||||
test('rejects metadata-only embedded LRC', () {
|
||||
const raw = '''
|
||||
|
||||
Reference in New Issue
Block a user