tests.test_create_album_program   A
last analyzed

Complexity

Total Complexity 5

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Test Coverage

Coverage 50%

Importance

Changes 0
Metric Value
eloc 172
dl 0
loc 272
ccs 12
cts 24
cp 0.5
rs 10
c 0
b 0
f 0
wmc 5

3 Functions

Rating   Name   Duplication   Size   Complexity  
A test_invoking_cli_with_help_flag() 0 13 1
A valid_youtube_videos() 0 28 1
B test_integration() 0 215 3
1 1
import os
2 1
from unittest import mock
3
4 1
import pytest
5 1
6 1
from music_album_creation.create_album import main
7 1
8
9
def test_invoking_cli_with_help_flag(run_subprocess):
10 1
    """Smoke test to ensure that the CLI can be invoked with the help flag"""
11 1
    import sys
12 1
13
    result = run_subprocess(
14
        sys.executable,
15 1
        '-m',
16
        'music_album_creation',
17
        '--help',
18
        check=False,  # we disable check, because we do it in unit test below
19 1
    )
20 1
    assert result.stderr == ''
21
    assert result.exit_code == 0
22
23
24
@pytest.fixture
25
def valid_youtube_videos():
26
    """Youtube video urls and their expected mp3 name to be 'downloaded'.
27
28
    Note:
29
        Maintain this fixture in cases such as a youtube video title changing
30
        over time, or a youtube url ceasing to exist.
31
32
    Returns:
33
        [type]: [description]
34
    """
35
    from collections import namedtuple
36
37
    YoutubeVideo = namedtuple('YoutubeVideo', ['url', 'video_title'])
38
    return (
39
        YoutubeVideo(url, video_title)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable url does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable video_title does not seem to be defined.
Loading history...
40
        for url, video_title in [
41
            (
42
                'https://www.youtube.com/watch?v=jJkF3I5cBAc',
43
                '10 seconds countdown (video test)',
44
            ),
45
            (
46
                'https://www.youtube.com/watch?v=Q3dvbM6Pias',
47
                'Rage Against The Machine - Testify (Official HD Video)',
48
            ),
49
            (
50
                'https://www.youtube.com/watch?v=G95sOBFkRxs',
51
                'Legalize It',  # Cypress Hill 1080p HD 0:46
52
            ),
53
        ]
54
    )
55
56
57
@pytest.mark.network_bound("Makes a request to youtube.com, thus using network")
58
@pytest.mark.runner_setup(mix_stderr=False)
59
@mock.patch('music_album_creation.create_album.inout')
60
@mock.patch('music_album_creation.create_album.music_lib_directory')
61
def test_integration(
62
    mock_music_lib_directory,
63
    mock_inout,
64
    tmp_path_factory,
65
    isolated_cli_runner,
66
    valid_youtube_videos,
67
    get_object,
68
):
69
    download_dir = tmp_path_factory.mktemp('download_dir')
70
71
    assert os.listdir(download_dir) == []
72
    target_directory = str(tmp_path_factory.mktemp('temp-music-library'))
73
74
    mock_music_lib_directory.return_value = target_directory
75
    # GIVEN a youtube URL
76
    mock_inout.input_youtube_url_dialog.return_value = list(valid_youtube_videos)[2].url
77
    mock_inout.interactive_track_info_input_dialog.return_value = (
78
        '1.  Gasoline - 0:00\n' '2.  Man vs. God - 0:07\n'
79
    )
80
    expected_album_dir: str = os.path.join(target_directory, 'del/Faith_in_Science')
81
    mock_inout.track_information_type_dialog.return_value = 'Timestamps'
82
    mock_inout.album_directory_path_dialog.return_value = expected_album_dir
83
    user_input_metadata = {
84
        'artist': 'Test Artist',
85
        'album-artist': 'Test Artist',
86
        'album': 'Faith in Science',
87
        'year': '2019',
88
    }
89
    mock_inout.interactive_metadata_dialogs.return_value = user_input_metadata
90
    # Configure MusicMaster to download to our desired directory
91
    from music_album_creation.music_master import MusicMaster as MM
92
93
    def MusicMaster(*args, **kwargs):
94
        music_master = MM(*args, **kwargs)
95
        music_master.download_dir = download_dir
96
        return music_master
97
98
    # Monkey patch at the module level
99
    get_object(
100
        'main',
101
        'music_album_creation.create_album',
102
        overrides={'MusicMaster': lambda: MusicMaster},
103
    )
104
105
    result = isolated_cli_runner.invoke(
106
        main,
107
        args=None,
108
        input=None,
109
        env=None,
110
        catch_exceptions=False,
111
        color=False,
112
        **{},
113
    )
114
    print(result.stdout)
115
    print(result.stderr)
116
    assert result.stderr == ''
117
    assert result.exit_code == 0
118
    print("CAP\n{}\nCAP".format(result.output))
119
    # captured = capsys.readouterr()
120
121
    # AND the album directory should be created
122
    assert os.path.isdir(expected_album_dir)
123
124
    # AND the album directory should contain the expected tracks
125
    expected_tracks = {
126
        '01 - Gasoline.mp3',
127
        '02 - Man vs. God.mp3',
128
    }
129
    assert set(os.listdir(expected_album_dir)) == expected_tracks
130
131
    # AND downloaded youtube video is found in the download directory
132
    assert len(os.listdir(download_dir)) == 1
133
    # AND the downloaded youtube file name is the expected
134
    print(os.listdir(download_dir))
135
    expected_file_name = 'Legalize It.mp4'
136
    assert os.listdir(download_dir)[0] == expected_file_name
137
138
    # READ downloaded stream file metadata to compare with segmented tracks
139
    from music_album_creation.ffmpeg import FFProbe
140
    from music_album_creation.ffprobe_client import FFProbeClient
141
142
    ffprobe = FFProbe(os.environ.get('MUSIC_FFPROBE', 'ffprobe'))
143
    ffprobe_client = FFProbeClient(ffprobe)
144
145
    BYTE_SIZE_ERROR_MARGIN = 0.01
146
    expected_bytes_size = 762505
147
    # download_dir
148
    downloaded_file = download_dir / expected_file_name
149
    # AND its metadata
150
    original_data = ffprobe_client.get_stream_info(str(downloaded_file))
151
    import json
152
153
    print(json.dumps(original_data, indent=4, sort_keys=True))
154
    # sanity check on metadata values BEFORE segmenting
155
    assert original_data['programs'] == []
156
    assert len(original_data['streams']) == 1
157
158
    assert original_data['streams'][0]['tags'] == {}
159
    # AND the audio stream has the expected Sample Rate (Hz)
160
    assert original_data['streams'][0]['sample_rate'] == '44100'
161
162
    # AND the audio stream has the expected codec
163
    assert original_data['streams'][0]['codec_name'] == 'aac'
164
165
    # AND the audio stream has the expected number of channels
166
    assert original_data['streams'][0]['channels'] == 2
167
168
    assert original_data['format']['format_name'] == 'mov,mp4,m4a,3gp,3g2,mj2'
169
    assert original_data['format']['format_long_name'] == 'QuickTime / MOV'
170
    assert original_data['format']['start_time'] == '-0.036281'
171
    assert original_data['format']['duration'] == '46.933333'
172
    assert original_data['format']['size'] == str(expected_bytes_size)
173
    assert downloaded_file.stat().st_size == expected_bytes_size
174
    assert original_data['format']['bit_rate'] == '129972'  # bits per second
175
    assert original_data['format']['probe_score'] == 100
176
    assert 'encoder' not in original_data['format']['tags']
177
    # assert original_data['format']['tags']['encoder'] == 'google/video-file'
178
179
    # AND the maths add up (size = track duration * bitrate)
180
    assert (
181
        int(original_data['format']['size'])
182
        >= 0.9
183
        * int(original_data['format']['bit_rate'])
184
        * float(original_data['format']['duration'])
185
        / 8
186
    )
187
    assert (
188
        int(original_data['format']['size'])
189
        <= 1.1
190
        * int(original_data['format']['bit_rate'])
191
        * float(original_data['format']['duration'])
192
        / 8
193
    )
194
195
    expected_durations = (7, 39)
196
    expected_sizes = (114010, 642005)  # in Bytes
197
    exp_bitrates = (129797, 128421)
198
199
    for track, expected_duration, expected_size, exp_bitrate in zip(
200
        sorted(list(expected_tracks)), expected_durations, expected_sizes, exp_bitrates
201
    ):
202
        # AND each track should have the expected metadata
203
        track_path = os.path.join(expected_album_dir, track)
204
        assert os.path.exists(track_path)
205
206
        # AND the track should have the expected size (within a window of error of 1%)
207
        assert abs(
208
            os.path.getsize(track_path) - expected_size
209
        ) <= BYTE_SIZE_ERROR_MARGIN * os.path.getsize(track_path)
210
211
        # assert os.path.getsize(track_path) == expected_size
212
213
        data = ffprobe_client.get_stream_info(track_path)
214
        import json
215
216
        # as reported by our metadata reading function
217
        track_byte_size = int(data['format']['size'])
218
219
        print(json.dumps(data, indent=4, sort_keys=True))
220
        assert data['programs'] == []
221
        assert len(data['streams']) == 1
222
        assert data['streams'][0]['tags'] == {}
223
        # AND the track file has the same stream quality as original  audio stream
224
        assert data['streams'][0]['sample_rate'] == '44100'
225
        assert data['streams'][0]['codec_name'] == 'mp3'
226
        assert data['streams'][0]['channels'] == 2
227
        assert data['format']['format_name'] == 'mp3'
228
        assert data['format']['format_long_name'] == 'MP2/3 (MPEG audio layer 2/3)'
229
        # assert data['format']['start_time'] == '-0.007000'
230
        assert abs(float(data['format']['duration']) - expected_duration) < 1
231
        # AND the track should have the expected size (within a window of error of 1%)
232
        assert abs(track_byte_size - expected_size) <= BYTE_SIZE_ERROR_MARGIN * track_byte_size
233
234
        reported_bitrate = int(data['format']['bit_rate'])
235
        # assert data['format']['bit_rate'] == str(exp_bitrate)  # bits per second
236
        assert abs(reported_bitrate - exp_bitrate) < 0.02 * reported_bitrate
237
238
        # assert data['format']['probe_score'] == 100
239
        assert data['format']['probe_score'] == 51
240
        assert data['format']['tags']['encoder'] == 'Lavf58.76.100'
241
242
        # AND maths add up (size = track duration * bitrate)
243
        estimated_size = (
244
            int(data['format']['bit_rate']) * float(data['format']['duration']) / 8
245
        )
246
        assert abs(int(data['format']['size']) - estimated_size) < 0.01 * estimated_size
247
248
        # AND bitrate has not changed more than 5% compared to original
249
        assert abs(
250
            int(data['format']['bit_rate']) - int(original_data['format']['bit_rate'])
251
        ) < 0.05 * int(original_data['format']['bit_rate'])
252
253
        # AND file size is proportional to duration (track byte size = track duration * bitrate)
254
        estimated_track_byte_size = (
255
            expected_bytes_size
256
            * expected_duration
257
            / float(original_data['format']['duration'])
258
        )
259
        assert (
260
            abs(int(data['format']['size']) - estimated_track_byte_size)
261
            < 0.05 * estimated_track_byte_size
262
        )
263
264
        # AND the artist tag is set to the expected artist
265
        assert data['format']['tags']['artist'] == user_input_metadata['artist']
266
        # AND the album_artist tag is set to the expected album_artist
267
        assert data['format']['tags']['album_artist'] == user_input_metadata['album-artist']
268
        # AND the album tag is set to the expected album
269
        assert data['format']['tags']['album'] == user_input_metadata['album']
270
        # AND the year tag is set to the expected year
271
        assert data['format']['tags']['date'] == user_input_metadata['year']
272