Commit 10cf95ec authored by Sam Thursfield's avatar Sam Thursfield

Merge branch 'sam/cuesheet-testcase' into 'master'

Add testcase for cue sheet extractor

See merge request !58
parents afb0d921 b09116a7
Pipeline #79578 passed with stage
in 1 minute and 12 seconds
......@@ -25,14 +25,14 @@ directory (containing xxx.expected files)
"""
from common.utils import configuration as cfg
from common.utils.extractor import get_tracker_extract_jsonld_output
from common.utils.extractor import get_tracker_extract_jsonld_output, TrackerExtractTestCase
import unittest as ut
import json
import os
import sys
class ExtractionTestCase (ut.TestCase):
class GenericExtractionTestCase(TrackerExtractTestCase):
"""
Test checks if the tracker extractor is able to retrieve metadata
"""
......@@ -41,7 +41,7 @@ class ExtractionTestCase (ut.TestCase):
"""
Descfile is the description file in a relative path
"""
ut.TestCase.__init__(self, methodName)
super(GenericExtractionTestCase, self).__init__(methodName)
self.descfile = descfile
try:
......@@ -94,106 +94,11 @@ class ExtractionTestCase (ut.TestCase):
else:
raise Exception("Unexpected success. Check " + self.rel_description)
def assertDictHasKey(self, d, key, msg=None):
if not isinstance(d, dict):
self.fail("Expected dict, got %s" % d)
if key not in d:
standardMsg = "Missing: %s\n" % (key)
self.fail(self._formatMessage(msg, standardMsg))
else:
return
def assertIsURN(self, supposed_uuid, msg=None):
import uuid
try:
if (supposed_uuid.startswith("<") and supposed_uuid.endswith(">")):
supposed_uuid = supposed_uuid[1:-1]
uuid.UUID(supposed_uuid)
except ValueError:
standardMsg = "'%s' is not a valid UUID" % (supposed_uuid)
self.fail(self._formatMessage(msg, standardMsg))
def __assert_extraction_ok(self, result):
try:
self.__check(self.spec['metadata'], result)
self.assert_extract_result_matches_spec(self.spec['metadata'], result, self.file_to_extract, self.descfile)
except AssertionError as e:
print("\ntracker-extract returned: %s" % json.dumps(result, indent=4))
raise
def __check(self, spec, result):
error_missing_prop = "Property '%s' hasn't been extracted from file \n'%s'\n (requested on '%s')"
error_wrong_value = "on property '%s' from file %s\n (requested on: '%s')"
error_wrong_length = "Length mismatch on property '%s' from file %s\n (requested on: '%s')"
error_extra_prop = "Property '%s' was explicitely banned for file \n'%s'\n (requested on '%s')"
error_extra_prop_v = "Property '%s' with value '%s' was explicitely banned for file \n'%s'\n (requested on %s')"
expected_pairs = [] # List of expected (key, value)
unexpected_pairs = [] # List of unexpected (key, value)
expected_keys = [] # List of expected keys (the key must be there, value doesnt matter)
for k, v in list(spec.items()):
if k.startswith("!"):
unexpected_pairs.append((k[1:], v))
elif k == '@type':
expected_keys.append('@type')
else:
expected_pairs.append((k, v))
for prop, expected_value in expected_pairs:
self.assertDictHasKey(result, prop,
error_missing_prop % (prop,
self.file_to_extract,
self.descfile))
if expected_value == "@URNUUID@":
self.assertIsURN(result[prop][0]['@id'],
error_wrong_value % (prop,
self.file_to_extract,
self.descfile))
else:
if isinstance(expected_value, list):
if not isinstance(result[prop], list):
raise AssertionError("Expected a list property for %s, but got a %s: %s" % (
prop, type(result[prop]).__name__, result[prop]))
self.assertEqual(len(expected_value), len(result[prop]),
error_wrong_length % (prop,
self.file_to_extract,
self.descfile))
for i in range(0, len(expected_value)):
self.__check(spec[prop][i], result[prop][i])
elif isinstance(expected_value, dict):
self.__check(expected_value, result[prop])
else:
self.assertEqual(str(spec[prop]), str(result[prop]),
error_wrong_value % (prop,
self.file_to_extract,
self.descfile))
for (prop, value) in unexpected_pairs:
# There is no prop, or it is but not with that value
if (value == ""):
self.assertFalse(prop in result, error_extra_prop % (prop,
self.file_to_extract,
self.descfile))
else:
if (value == "@URNUUID@"):
self.assertIsURN(result[prop][0], error_extra_prop % (prop,
self.file_to_extract,
self.descfile))
else:
self.assertNotIn(value, result[prop], error_extra_prop_v % (prop,
value,
self.file_to_extract,
self.descfile))
for prop in expected_keys:
self.assertDictHasKey(result, prop,
error_missing_prop % (prop,
self.file_to_extract,
self.descfile))
def run_all():
......@@ -215,7 +120,7 @@ def run_all():
for root, dirs, files in os.walk(TEST_DATA_PATH):
descriptions = [os.path.join(root, f) for f in files if f.endswith("expected")]
for descfile in descriptions:
tc = ExtractionTestCase(descfile=descfile)
tc = GenericExtractionTestCase(descfile=descfile)
extractionTestSuite.addTest(tc)
result = ut.TextTestRunner(verbosity=1).run(extractionTestSuite)
sys.exit(not result.wasSuccessful())
......@@ -228,7 +133,7 @@ def run_one(filename):
description = os.path.join(os.getcwd(), filename)
extractionTestSuite = ut.TestSuite()
tc = ExtractionTestCase(descfile=description)
tc = GenericExtractionTestCase(descfile=description)
extractionTestSuite.addTest(tc)
result = ut.TextTestRunner(verbosity=2).run(extractionTestSuite)
......
#!/usr/bin/env python3
# Copyright (C) 2019, Sam Thursfield (sam@afuera.me.uk)
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.
"""
Tests the FLAC+cuesheet extraction feature.
"""
import os
import shutil
import tempfile
import unittest as ut
import common.utils.configuration as cfg
from common.utils.helpers import log
from common.utils.extractor import get_tracker_extract_jsonld_output, create_test_flac, TrackerExtractTestCase
class FlacCuesheetTest(TrackerExtractTestCase):
def spec(self, audio_path):
audio_uri = 'file://' + audio_path
return {
'@type': ['nfo:Audio'],
'nie:url': audio_uri,
'nfo:duration': 360,
'nfo:sampleRate': 44100,
'nie:hasLogicalPart': [
{
'@type': ['nmm:MusicPiece', 'nfo:Audio'],
'nfo:audioOffset': 0.0,
'nfo:duration': 257,
'nie:isLogicalPartOf': audio_uri,
'nie:isStoredAs': audio_uri,
'nie:title': 'Only Shallow',
'nmm:trackNumber': 1,
'nmm:musicAlbum': {
'@id': 'urn:album:Loveless:My%20Bloody%20Valentine',
'@type': 'nmm:MusicAlbum',
'nmm:albumTrackCount': 2,
'nmm:albumArtist': ['urn:artist:My%20Bloody%20Valentine'],
'nie:title': 'Loveless',
},
'nmm:musicAlbumDisc': {
'@id': 'urn:album-disc:Loveless:My%20Bloody%20Valentine:Disc1',
'@type': 'nmm:MusicAlbumDisc',
'nmm:setNumber': 1,
'nmm:albumDiscAlbum': 'urn:album:Loveless:My%20Bloody%20Valentine',
},
'nmm:performer': {
'@id': 'urn:artist:My%20Bloody%20Valentine',
'@type': 'nmm:Artist',
'nmm:artistName': 'My Bloody Valentine',
},
},
{
'@type': ['nmm:MusicPiece', 'nfo:Audio'],
'nfo:audioOffset': 257.6933333333333,
'nfo:duration': 102,
'nie:isLogicalPartOf': audio_uri,
'nie:isStoredAs': audio_uri,
'nmm:musicAlbum': 'urn:album:Loveless:My%20Bloody%20Valentine',
'nmm:musicAlbumDisc': 'urn:album-disc:Loveless:My%20Bloody%20Valentine:Disc1',
'nmm:performer': 'urn:artist:My%20Bloody%20Valentine',
'nie:title': 'Loomer',
'nmm:trackNumber': 2,
}
],
}
def test_external_cue_sheet(self):
with tempfile.TemporaryDirectory() as tmpdir:
datadir = os.path.join(os.getcwd() + "/test-extraction-data")
shutil.copy(os.path.join(datadir, 'audio', 'cuesheet-test.cue'), tmpdir)
audio_path = os.path.join(tmpdir, 'cuesheet-test.flac')
create_test_flac(audio_path, duration=6*60)
result = get_tracker_extract_jsonld_output(audio_path)
self.assert_extract_result_matches_spec(
self.spec(audio_path), result, audio_path, __file__)
if __name__ == '__main__':
ut.main()
#!/usr/bin/env python3
#
# Copyright (C) 2010, Nokia <ivan.frade@nokia.com>
# Copyright (C) 2018, Sam Thursfield <sam@afuera.me.uk>
# Copyright (C) 2018-2019, Sam Thursfield <sam@afuera.me.uk>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
......@@ -23,9 +23,15 @@ from common.utils import configuration as cfg
from common.utils.helpers import log
import errno
import json
import math
import os
import re
import subprocess
import unittest as ut
import gi
gi.require_version('Gst', '1.0')
from gi.repository import GLib, Gst
def get_tracker_extract_jsonld_output(filename, mime_type=None):
......@@ -70,3 +76,140 @@ def get_tracker_extract_jsonld_output(filename, mime_type=None):
"Output was: %s" % (e, output))
return data
class TrackerExtractTestCase(ut.TestCase):
def assertDictHasKey(self, d, key, msg=None):
if not isinstance(d, dict):
self.fail("Expected dict, got %s" % d)
if key not in d:
standardMsg = "Missing: %s\n" % (key)
self.fail(self._formatMessage(msg, standardMsg))
else:
return
def assertIsURN(self, supposed_uuid, msg=None):
import uuid
try:
if (supposed_uuid.startswith("<") and supposed_uuid.endswith(">")):
supposed_uuid = supposed_uuid[1:-1]
uuid.UUID(supposed_uuid)
except ValueError:
standardMsg = "'%s' is not a valid UUID" % (supposed_uuid)
self.fail(self._formatMessage(msg, standardMsg))
def assert_extract_result_matches_spec(self, spec, result, filename, spec_filename):
"""
Checks tracker-extract json-ld output against the expected result.
Use get_tracker_extract_jsonld_output() to get the extractor output.
Look in test-extraction-data/*/*.expected.json for examples of the spec
format.
"""
error_missing_prop = "Property '%s' hasn't been extracted from file \n'%s'\n (requested on '%s')"
error_wrong_value = "on property '%s' from file %s\n (requested on: '%s')"
error_wrong_length = "Length mismatch on property '%s' from file %s\n (requested on: '%s')"
error_extra_prop = "Property '%s' was explicitely banned for file \n'%s'\n (requested on '%s')"
error_extra_prop_v = "Property '%s' with value '%s' was explicitely banned for file \n'%s'\n (requested on %s')"
expected_pairs = [] # List of expected (key, value)
unexpected_pairs = [] # List of unexpected (key, value)
expected_keys = [] # List of expected keys (the key must be there, value doesnt matter)
for k, v in list(spec.items()):
if k.startswith("!"):
unexpected_pairs.append((k[1:], v))
elif k == '@type':
expected_keys.append('@type')
else:
expected_pairs.append((k, v))
for prop, expected_value in expected_pairs:
self.assertDictHasKey(result, prop,
error_missing_prop % (prop, filename, spec_filename))
if expected_value == "@URNUUID@":
self.assertIsURN(result[prop][0]['@id'],
error_wrong_value % (prop, filename, spec_filename))
else:
if isinstance(expected_value, list):
if not isinstance(result[prop], list):
raise AssertionError("Expected a list property for %s, but got a %s: %s" % (
prop, type(result[prop]).__name__, result[prop]))
self.assertEqual(len(expected_value), len(result[prop]),
error_wrong_length % (prop, filename, spec_filename))
for i in range(0, len(expected_value)):
if isinstance(expected_value[i], dict):
self.assert_extract_result_matches_spec(expected_value[i], result[prop][i], filename, spec_filename)
else:
self.assertEqual(str(expected_value[i]), str(result[prop][i]),
error_wrong_value % (prop, filename, spec_filename))
elif isinstance(expected_value, dict):
self.assert_extract_result_matches_spec(expected_value, result[prop], filename, spec_filename)
else:
self.assertEqual(str(spec[prop]), str(result[prop]),
error_wrong_value % (prop, filename, spec_filename))
for (prop, value) in unexpected_pairs:
# There is no prop, or it is but not with that value
if (value == ""):
self.assertFalse(prop in result,
error_extra_prop % (prop, filename, spec_filename))
else:
if (value == "@URNUUID@"):
self.assertIsURN(result[prop][0],
error_extra_prop % (prop, filename, spec_filename))
else:
self.assertNotIn(value, result[prop],
error_extra_prop_v % (prop, value, filename, spec_filename))
for prop in expected_keys:
self.assertDictHasKey(result, prop,
error_missing_prop % (prop, filename, spec_filename))
def create_test_flac(path, duration, timeout=10):
"""
Create a .flac audio file for testing purposes.
FLAC audio doesn't compress test data particularly efficiently, so
committing an audio file more than a few seconds long to Git is not
practical. This function creates a .flac file containing a test tone.
The 'duration' parameter sets the length in seconds of the time.
The function is guaranteed to return or raise an exception within the
number of seconds given in the 'timeout' parameter.
"""
Gst.init([])
num_buffers = math.ceil(duration * 44100 / 1024.0)
pipeline_src = ' ! '.join([
'audiotestsrc num-buffers=%s samplesperbuffer=1024' % num_buffers,
'capsfilter caps="audio/x-raw,rate=44100"',
'flacenc',
'filesink location=%s' % path,
])
log("Running pipeline: %s" % pipeline_src)
pipeline = Gst.parse_launch(pipeline_src)
ret = pipeline.set_state(Gst.State.PLAYING)
msg = pipeline.get_bus().poll(Gst.MessageType.ERROR | Gst.MessageType.EOS,
timeout * Gst.SECOND)
if msg and msg.type == Gst.MessageType.EOS:
pass
elif msg and msg.type == Gst.MessageType.ERROR:
raise RuntimeError(msg.parse_error())
elif msg:
raise RuntimeError("Got unexpected GStreamer message %s" % msg.type)
else:
raise RuntimeError("Timeout generating test audio file after %i seconds" % timeout)
pipeline.set_state(Gst.State.NULL)
......@@ -30,6 +30,7 @@ functional_tests = [
'310-fts-basic',
'311-fts-file-operations',
'312-fts-stopwords',
'401-extractor-flac-cuesheet',
'410-extractor-decorator',
'500-writeback',
'501-writeback-details',
......
REM Example CUE sheet adapted from http://wiki.hydrogenaud.io/index.php?title=Cue_sheet#Examples
REM GENRE Alternative
REM DATE 1991
REM DISCID 860B640B
REM COMMENT "ExactAudioCopy v0.95b4"
PERFORMER "My Bloody Valentine"
TITLE "Loveless"
FILE "cuesheet-test.cue" WAVE
TRACK 01 AUDIO
TITLE "Only Shallow"
PERFORMER "My Bloody Valentine"
INDEX 01 00:00:00
TRACK 02 AUDIO
TITLE "Loomer"
PERFORMER "My Bloody Valentine"
INDEX 01 04:17:52
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment