test: Add a test for media engines
[profile/ivi/rygel.git] / tests / rygel-media-engine-test.vala
1 /*
2  * Copyright (C) 2012 Intel Corporation.
3  *
4  * Author: Jens Georg <jensg@openismus.com>
5  *
6  * This file is part of Rygel.
7  *
8  * Rygel is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU Lesser General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * Rygel is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public License
19  * along with this program; if not, write to the Free Software Foundation,
20  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21  */
22
23 [CCode (cname="TEST_DATA_FOLDER")]
24 extern const string TEST_DATA_FOLDER;
25
26 // Always test locally built engines
27 [CCode (cname="TEST_ENGINE_PATH")]
28 extern const string TEST_ENGINE_PATH;
29
30 [CCode (cname="BUILT_ENGINES")]
31 extern const string BUILT_ENGINES;
32
33 /**
34  * Helper class to convince the engine loader to load the media engine we want
35  * to test.
36  */
37 internal class Rygel.DataSourceTestConfig : Rygel.BaseConfiguration {
38     private string engine;
39     private string path;
40
41     public DataSourceTestConfig (string? path = null,
42                                  string? engine = null) {
43         this.engine = engine;
44         this.path = path;
45     }
46
47     public override string get_media_engine () throws Error {
48         if (this.engine != null) {
49             return this.engine;
50         }
51
52         // Throw error
53         return base.get_media_engine ();
54     }
55
56     public override string get_engine_path () throws Error {
57         if (this.path != null) {
58             return this.path;
59         }
60
61         return TEST_ENGINE_PATH;
62     }
63
64     public string to_string () {
65         return "Path: %s, Engine: %s".printf (this.path, this.engine);
66     }
67
68     public void clear () {
69         this.engine = null;
70         this.path = null;
71     }
72 }
73
74 /**
75  * Stub implementation of Rygel.HTTPSeek
76  */
77 internal class Rygel.ByteSeek : Rygel.HTTPSeek {
78     public ByteSeek (int64 first, int64 last, int64 length) {
79         var msg = new Soup.Message ("GET", "http://example.com/");
80         base (msg, first, last, 1, length);
81         this.seek_type = HTTPSeekType.BYTE;
82     }
83
84     public override void add_response_headers () {}
85 }
86
87 /**
88  * Wrapper class arount uint8[] arrays to help stuff those buffers into a
89  * Gee.ArrayList
90  */
91 class DataBlock {
92     public uint8[] data;
93
94     public DataBlock (uint8[] data) {
95         this.data = data;
96     }
97 }
98
99 /**
100  * Helper class to collect a number of byte buffers
101  */
102 internal class Rygel.DataPool : Gee.ArrayList<DataBlock> {
103     public uint8[] flatten () {
104         var size = this.total_size ();
105         var result = new uint8[size];
106         var offset = 0;
107
108         foreach (var data in this) {
109             Memory.copy (result + offset, (void *) data.data, data.data.length);
110             offset += data.data.length;
111         }
112
113         this.clear ();
114         this.add (new DataBlock (result));
115
116         return result;
117     }
118
119     public uint64 total_size () {
120         uint64 total = 0;
121         foreach (var data in this) {
122             total += data.data.length;
123         }
124
125         return total;
126     }
127 }
128
129 /**
130  * Test a DataSource implementation against the expectations of the interface
131  *
132  * It is run as part of the test suite but can be used to check arbitrary
133  * media engines as well:
134  *
135  * rygel-media-engine-test /path/to/my/first/custom-rygel-engine.so \
136  *                         /path/to/my/second/custom-rygel-engine.so ...
137  */
138 public class Rygel.DataSourceTest : Object {
139     private File test_data_file;
140     private MappedFile test_data_mapped;
141
142     public DataSourceTest () {
143         var path = Path.build_filename (TEST_DATA_FOLDER, "test-data.dat");
144         this.test_data_file = File.new_for_path (path);
145         try {
146             this.test_data_mapped = new MappedFile (path, false);
147         } catch (Error error) {
148             warning ("Error: Could not map file: %s", error.message);
149             assert_not_reached ();
150         }
151     }
152
153     /// Get the whole file
154     private void test_simple_streaming () {
155         debug ("test_simple_streaming");
156         var source = MediaEngine.get_default ().create_data_source
157                                         (this.test_data_file.get_uri ());
158         // Sources should support file:// urls
159         assert (source != null);
160
161         uint64 received_bytes = 0;
162         var loop = new MainLoop (null, false);
163         source.data_available.connect ( (data) => {
164             received_bytes += data.length;
165         });
166         source.done.connect ( (data) => {
167             loop.quit ();
168         });
169         Idle.add ( () => { source.start (null); return false; });
170         loop.run ();
171         assert (received_bytes == this.test_data_mapped.get_length ());
172         source.stop ();
173         source = null;
174     }
175
176     /// Simple byte range request tests
177     private void test_byte_range_request () {
178         debug ("test_byte_range_request");
179         var source = MediaEngine.get_default ().create_data_source
180                                         (this.test_data_file.get_uri ());
181         // Sources should support file:// urls
182         assert (source != null);
183
184         try {
185             // Get the first 10 bytes
186             var seek = new ByteSeek (0, 9, this.test_data_mapped.get_length ());
187
188             var received_data = new DataPool ();
189             var loop = new MainLoop (null, false);
190             source.data_available.connect ( (data) => {
191                 received_data.add (new DataBlock (data));
192             });
193             source.done.connect ( (data) => {
194                 loop.quit ();
195             });
196             source.error.connect ( () => { assert_not_reached (); });
197             source.start (seek);
198             loop.run ();
199             assert (received_data.total_size () == 10);
200             Memory.cmp (this.test_data_mapped.get_contents (),
201                         received_data.flatten (),
202                         (size_t) received_data.total_size ());
203
204             // Get last 10 bytes
205             seek = new ByteSeek (this.test_data_mapped.get_length () - 10,
206                                  this.test_data_mapped.get_length () - 1,
207                                  this.test_data_mapped.get_length ());
208
209             received_data = new DataPool ();
210             loop = new MainLoop (null, false);
211
212             source = MediaEngine.get_default ().create_data_source
213                                         (this.test_data_file.get_uri ());
214             source.data_available.connect ( (data) => {
215                 received_data.add (new DataBlock (data));
216             });
217             source.done.connect ( (data) => {
218                 loop.quit ();
219             });
220             source.error.connect ( () => { assert_not_reached (); });
221             source.start (seek);
222             loop.run ();
223
224             assert (received_data.total_size () == 10);
225             Memory.cmp (this.test_data_mapped.get_contents () +
226                         (this.test_data_mapped.get_length () - 10),
227                         received_data.flatten (),
228                         (size_t) received_data.total_size ());
229
230             // Get something from the middle
231             seek = new ByteSeek (this.test_data_mapped.get_length () / 2,
232                                  (this.test_data_mapped.get_length () / 2) + 9,
233                                  this.test_data_mapped.get_length ());
234
235             received_data = new DataPool ();
236             loop = new MainLoop (null, false);
237
238             source = MediaEngine.get_default ().create_data_source
239                                         (this.test_data_file.get_uri ());
240             source.data_available.connect ( (data) => {
241                 received_data.add (new DataBlock (data));
242             });
243
244             source.done.connect ( (data) => {
245                 loop.quit ();
246             });
247             source.error.connect ( () => { assert_not_reached (); });
248             source.start (seek);
249             loop.run ();
250
251             assert (received_data.total_size () == 10);
252             Memory.cmp (this.test_data_mapped.get_contents () +
253                         (this.test_data_mapped.get_length () / 2),
254                         received_data.flatten (),
255                         (size_t) received_data.total_size ());
256             source.stop ();
257             source = null;
258         } catch (DataSourceError.SEEK_FAILED seek_error) {
259             debug ("Skipping seek test");
260         } catch (Error error) {
261             warning ("Failed to test: %s", error.message);
262             assert_not_reached ();
263         }
264     }
265
266     // Check that calling start() after stop() starts at the beginning of the
267     // data
268     private void test_stop_start () {
269         debug ("test_stop_start");
270         var source = MediaEngine.get_default ().create_data_source
271                                         (this.test_data_file.get_uri ());
272         // Sources should support file:// urls
273         assert (source != null);
274
275         var pool = new DataPool ();
276         var loop = new MainLoop (null, false);
277         source.data_available.connect ( (data) => {
278             pool.add (new DataBlock (data));
279             source.stop ();
280         });
281         source.done.connect ( (data) => {
282             loop.quit ();
283         });
284         Idle.add ( () => { source.start (null); return false; });
285         loop.run ();
286         pool.clear ();
287         Idle.add ( () => { source.start (null); return false; });
288         loop.run ();
289         Memory.cmp (this.test_data_mapped.get_contents (),
290                     pool.flatten (),
291                     (size_t) pool.total_size ());
292
293         source.stop ();
294         source = null;
295     }
296
297     // Check that calling freeze multiple times only needs one thaw to get the
298     // data again
299     private void test_multiple_freeze () {
300         debug ("test_multiple_freeze");
301         var source = MediaEngine.get_default ().create_data_source
302                                         (this.test_data_file.get_uri ());
303         // Sources should support file:// urls
304         assert (source != null);
305         var available_id = source.data_available.connect ( () => {
306             assert_not_reached ();
307         });
308         source.start (null);
309         source.freeze ();
310         source.freeze ();
311         var loop = new MainLoop (null, false);
312
313         Timeout.add_seconds (5, () => {
314             loop.quit ();
315
316             return false;
317         });
318
319         loop.run ();
320         source.disconnect (available_id);
321         source.data_available.connect ( () => {
322             loop.quit ();
323         });
324
325         var timeout_id = Timeout.add_seconds (5, () => {
326             assert_not_reached ();
327
328             return false;
329         });
330
331         source.thaw ();
332         loop.run ();
333         Source.remove (timeout_id);
334         source.stop ();
335     }
336
337     // Check that it is possible to call stop() when the source is frozen and
338     // still get a done() signal
339     private void test_freeze_stop () {
340         debug ("test_freeze_stop");
341         var source = MediaEngine.get_default ().create_data_source
342                                         (this.test_data_file.get_uri ());
343         // Sources should support file:// urls
344         assert (source != null);
345
346         source.start (null);
347         source.freeze ();
348         var loop = new MainLoop (null, false);
349         source.done.connect ( () => {
350             loop.quit ();
351         });
352         var id = Timeout.add_seconds ( 5, () => {
353             assert_not_reached ();
354         });
355         Idle.add ( () => { source.stop (); return false; });
356         loop.run ();
357         Source.remove (id);
358         source.stop ();
359         source = null;
360     }
361
362     // Check that it is possible to stream to two targets in parallel
363     public void test_parallel_streaming () {
364         debug ("test_parallel_streaming");
365         var source1 = MediaEngine.get_default ().create_data_source
366                                         (this.test_data_file.get_uri ());
367         assert (source1 != null);
368         // Sources should support file:// urls
369         var source2 = MediaEngine.get_default ().create_data_source
370                                         (this.test_data_file.get_uri ());
371         assert (source2 != null);
372
373         source1.start (null);
374
375         var seek = new ByteSeek (0,
376                                  (this.test_data_mapped.get_length () / 2),
377                                  this.test_data_mapped.get_length ());
378
379         source2.start (seek);
380         var loop = new MainLoop (null, false);
381         var quit = false;
382         source1.done.connect ( () => {
383             if (quit) {
384                 loop.quit ();
385             } else {
386                 quit = true;
387             }
388         });
389
390         source2.done.connect ( () => {
391             if (quit) {
392                 loop.quit ();
393             } else {
394                 quit = true;
395             }
396         });
397         loop.run ();
398     }
399
400     public int run () {
401         this.test_simple_streaming ();
402         this.test_byte_range_request ();
403         this.test_stop_start ();
404         this.test_multiple_freeze ();
405         this.test_freeze_stop ();
406         this.test_parallel_streaming ();
407
408         return 0;
409     }
410
411     public static int main (string[] args) {
412         string[] engines;
413
414         var configs = new Gee.ArrayList<DataSourceTestConfig> ();
415
416         if (args.length > 1) {
417             foreach (var arg in args) {
418                 File file;
419                 if (args[1].has_prefix ("~")) {
420                     file = File.parse_name (args[1]);
421                 } else {
422                    file = File.new_for_commandline_arg (args[1]);
423                 }
424                 var path = file.get_parent ().get_path ();
425                 var engine = file.get_basename ();
426
427                 configs.add (new DataSourceTestConfig (path, engine));
428             }
429         } else {
430             foreach (var engine in BUILT_ENGINES.split (";")) {
431                 var name = engine + "." + Module.SUFFIX;
432                 configs.add (new DataSourceTestConfig (null, name));
433             }
434         }
435
436         DataSourceTestConfig previous_config = null;
437         foreach (var config in configs) {
438             // Invalidate previous config so MetaConfig picks up the
439             // current one
440             if (previous_config != null) {
441                 previous_config.clear ();
442             }
443
444             debug ("=> Executing tests for config %s", config.to_string ());
445             MetaConfig.register_configuration (config);
446             previous_config = config;
447
448             try {
449                 MediaEngine.init ();
450             } catch (Error error) {
451                 assert_not_reached ();
452             }
453
454             var test = new DataSourceTest ();
455
456             var result = test.run ();
457             if (result != 0) {
458                 return result;
459             }
460         }
461
462         return 0;
463     }
464 }