diff options
author | Eric Wong <normalperson@yhbt.net> | 2013-08-24 09:54:45 +0000 |
---|---|---|
committer | Eric Wong <normalperson@yhbt.net> | 2013-08-24 09:54:45 +0000 |
commit | 3e09ac0c10c95bb24a08af62393b4f761e2743d0 (patch) | |
tree | 778dffa2ba8798503fc047db0feef6d65426ea22 /test | |
download | dtas-3e09ac0c10c95bb24a08af62393b4f761e2743d0.tar.gz |
Diffstat (limited to 'test')
-rw-r--r-- | test/covshow.rb | 30 | ||||
-rw-r--r-- | test/helper.rb | 76 | ||||
-rw-r--r-- | test/player_integration.rb | 121 | ||||
-rw-r--r-- | test/test_buffer.rb | 216 | ||||
-rw-r--r-- | test/test_format.rb | 61 | ||||
-rw-r--r-- | test/test_format_change.rb | 49 | ||||
-rw-r--r-- | test/test_player.rb | 37 | ||||
-rw-r--r-- | test/test_player_client_handler.rb | 86 | ||||
-rw-r--r-- | test/test_player_integration.rb | 199 | ||||
-rw-r--r-- | test/test_rg_integration.rb | 117 | ||||
-rw-r--r-- | test/test_rg_state.rb | 32 | ||||
-rw-r--r-- | test/test_sink.rb | 32 | ||||
-rw-r--r-- | test/test_sink_reader_play.rb | 49 | ||||
-rw-r--r-- | test/test_sink_tee_integration.rb | 34 | ||||
-rw-r--r-- | test/test_source.rb | 102 | ||||
-rw-r--r-- | test/test_unixserver.rb | 66 | ||||
-rw-r--r-- | test/test_util.rb | 15 |
17 files changed, 1322 insertions, 0 deletions
diff --git a/test/covshow.rb b/test/covshow.rb new file mode 100644 index 0000000..e50c368 --- /dev/null +++ b/test/covshow.rb @@ -0,0 +1,30 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +# +# this works with the __covmerge method in test/helper.rb +# run this file after all tests are run + +# load the merged dump data +res = Marshal.load(IO.binread("coverage.dump")) + +# Dirty little text formatter. I tried simplecov but the default +# HTML+JS is unusable without a GUI (I hate GUIs :P) and it would've +# taken me longer to search the Internets to find a plain-text +# formatter I like... +res.keys.sort.each do |filename| + cov = res[filename] + puts "==> #{filename} <==" + File.readlines(filename).each_with_index do |line, i| + n = cov[i] + if n == 0 # BAD + print(" *** 0 #{line}") + elsif n + printf("% 7u %s", n, line) + elsif line =~ /\S/ # probably a line with just "end" in it + print(" #{line}") + else # blank line + print "\n" # don't output trailing whitespace on blank lines + end + end +end diff --git a/test/helper.rb b/test/helper.rb new file mode 100644 index 0000000..e4643ae --- /dev/null +++ b/test/helper.rb @@ -0,0 +1,76 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +$stdout.sync = $stderr.sync = Thread.abort_on_exception = true + +# fork-aware coverage data gatherer, see also test/covshow.rb +if ENV["COVERAGE"] + require "coverage" + COVMATCH = %r{/lib/dtas\b.*rb\z} + COVTMP = File.open("coverage.dump", IO::CREAT|IO::RDWR) + COVTMP.binmode + COVTMP.sync = true + + def __covmerge + res = Coverage.result + + # we own this file (at least until somebody tries to use NFS :x) + COVTMP.flock(File::LOCK_EX) + + COVTMP.rewind + prev = COVTMP.read + prev = prev.empty? ? {} : Marshal.load(prev) + res.each do |filename, counts| + # filter out stuff that's not in our project + COVMATCH =~ filename or next + + merge = prev[filename] || [] + merge = merge + counts.each_with_index do |count, i| + count or next + merge[i] = (merge[i] || 0) + count + end + prev[filename] = merge + end + COVTMP.rewind + COVTMP.truncate(0) + COVTMP.write(Marshal.dump(prev)) + ensure + COVTMP.flock(File::LOCK_UN) + end + + Coverage.start + at_exit { __covmerge } +end + +gem 'minitest' +require 'minitest/autorun' +require "tempfile" + +FIFOS = [] +at_exit { FIFOS.each { |(pid,path)| File.unlink(path) if $$ == pid } } +def tmpfifo + tmp = Tempfile.new(%w(dtas-test .fifo)) + path = tmp.path + tmp.close! + assert system(*%W(mkfifo #{path})), "mkfifo #{path}" + FIFOS << [ $$, path ] + path +end + +require 'tmpdir' +class Dir + require 'fileutils' + def Dir.mktmpdir + begin + d = "#{Dir.tmpdir}/#$$.#{rand}" + Dir.mkdir(d) + rescue Errno::EEXIST + end while true + begin + yield d + ensure + FileUtils.remove_entry(d) + end + end +end unless Dir.respond_to?(:mktmpdir) diff --git a/test/player_integration.rb b/test/player_integration.rb new file mode 100644 index 0000000..6580194 --- /dev/null +++ b/test/player_integration.rb @@ -0,0 +1,121 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'dtas/player' +require 'dtas/state_file' +require 'yaml' +require 'tempfile' +require 'shellwords' +require 'timeout' + +module PlayerIntegration + def setup + sock_tmp = Tempfile.new(%w(dtas-test .sock)) + @state_tmp = Tempfile.new(%w(dtas-test .yml)) + @sock_path = sock_tmp.path + sock_tmp.close! + @player = DTAS::Player.new + @player.socket = @sock_path + @player.state_file = DTAS::StateFile.new(@state_tmp.path) + @player.bind + @out = Tempfile.new(%w(dtas-test .out)) + @err = Tempfile.new(%w(dtas-test .err)) + @out.sync = @err.sync = true + @pid = fork do + at_exit { @player.close } + ENV["SOX_OPTS"] = "#{ENV['SOX_OPTS']} -R" + unless $DEBUG + $stdout.reopen(@out) + $stderr.reopen(@err) + end + @player.run + end + + # null playback device with delay to simulate a real device + @fmt = DTAS::Format.new + @period = 0.01 + @period_size = @fmt.bytes_per_sample * @fmt.channels * @fmt.rate * @period + @cmd = "exec 2>/dev/null " \ + "ruby -e " \ + "\"b=%q();loop{STDIN.readpartial(#@period_size,b);sleep(#@period)}\"" + + # FIXME gross... + @player.instance_eval do + @sink_buf.close! + end + end + + module PlayerClient + def preq(args) + args = Shellwords.join(args) if Array === args + send(args, Socket::MSG_EOR) + end + end + + def client_socket + s = Socket.new(:AF_UNIX, :SOCK_SEQPACKET, 0) + s.connect(Socket.pack_sockaddr_un(@sock_path)) + s.extend(PlayerClient) + s + end + + def wait_pid_dead(pid, time = 5) + Timeout.timeout(time) do + begin + Process.kill(0, pid) + sleep(0.01) + rescue Errno::ESRCH + return + end while true + end + end + + def wait_files_not_empty(*files) + files = Array(files) + Timeout.timeout(5) { sleep(0.01) until files.all? { |f| f.size > 0 } } + end + + def default_sink_pid(s) + default_pid = Tempfile.new(%w(dtas-test .pid)) + pf = "echo $$ >> #{default_pid.path}; " + s.send("sink ed default command='#{pf}#@cmd'", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + default_pid + end + + def teardown + Process.kill(:TERM, @pid) if @pid + Process.waitall + refute File.exist?(@sock_path) + @state_tmp.close! + @out.close! if @out + @err.close! if @err + end + + def read_pid_file(file) + file.rewind + pid = file.read.to_i + assert_operator pid, :>, 0 + pid + end + + def tmp_noise(len = 5) + noise = Tempfile.open(%w(junk .sox)) + cmd = %W(sox -R -n -r44100 -c2 #{noise.path} synth #{len} pluck) + assert system(*cmd), cmd + [ noise, len ] + end + + def dethrottle_decoder(s) + s.send("sink ed default active=false", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + end + + def stop_playback(pid_file, s) + s.send("skip", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + pid = read_pid_file(pid_file) + wait_pid_dead(pid) + end +end diff --git a/test/test_buffer.rb b/test/test_buffer.rb new file mode 100644 index 0000000..32ae986 --- /dev/null +++ b/test/test_buffer.rb @@ -0,0 +1,216 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'stringio' +require 'dtas/buffer' + +class TestBuffer < Minitest::Unit::TestCase + def teardown + @to_close.each { |io| io.close unless io.closed? } + end + + def setup + @to_close = [] + end + + def pipe + ret = IO.pipe + @to_close.concat(ret) + ret + end + + def tmperr + olderr = $stderr + $stderr = newerr = StringIO.new + yield + newerr + ensure + $stderr = olderr + end + + def new_buffer + buf = DTAS::Buffer.new + @to_close << buf.to_io + @to_close << buf.wr + buf + end + + def test_set_buffer_size + buf = new_buffer + buf.buffer_size = DTAS::Buffer::MAX_SIZE + assert_equal DTAS::Buffer::MAX_SIZE, buf.buffer_size + end if defined?(DTAS::Buffer::MAX_SIZE) + + def test_buffer_size + buf = new_buffer + assert_operator buf.buffer_size, :>, 128 + buf.buffer_size = DTAS::Buffer::MAX_SIZE + assert_equal DTAS::Buffer::MAX_SIZE, buf.buffer_size + end if defined?(DTAS::Buffer::MAX_SIZE) + + def test_broadcast_1 + buf = new_buffer + r, w = IO.pipe + assert_equal :wait_readable, buf.broadcast([w]) + assert_equal 0, buf.bytes_xfer + buf.wr.write "HIHI" + assert_equal :wait_readable, buf.broadcast([w]) + assert_equal 4, buf.bytes_xfer + assert_equal :wait_readable, buf.broadcast([w]) + assert_equal 4, buf.bytes_xfer + tmp = [w] + r.close + buf.wr.write "HIHI" + newerr = tmperr { assert_nil buf.broadcast(tmp) } + assert_equal [], tmp + assert_match(%r{dropping}, newerr.string) + end + + def test_broadcast_tee + buf = new_buffer + return unless buf.respond_to?(:__broadcast_tee) + blocked = [] + a = pipe + b = pipe + buf.wr.write "HELLO" + assert_equal 4, buf.__broadcast_tee(blocked, [a[1], b[1]], 4) + assert_empty blocked + assert_equal "HELL", a[0].read(4) + assert_equal "HELL", b[0].read(4) + assert_equal 5, buf.__broadcast_tee(blocked, [a[1], b[1]], 5) + assert_empty blocked + assert_equal "HELLO", a[0].read(5) + assert_equal "HELLO", b[0].read(5) + max = '*' * a[0].pipe_size + assert_equal max.size, a[1].write(max) + assert_equal a[0].nread, a[0].pipe_size + a[1].nonblock = true + assert_equal 5, buf.__broadcast_tee(blocked, [a[1], b[1]], 5) + assert_equal [a[1]], blocked + a[1].nonblock = false + b[0].read(b[0].nread) + b[1].write(max) + t = Thread.new do + sleep 0.005 + [ a[0].read(max.size).size, b[0].read(max.size).size ] + end + assert_equal 5, buf.__broadcast_tee(blocked, [a[1], b[1]], 5) + assert_equal [a[1]], blocked + assert_equal [ max.size, max.size ], t.value + b[0].close + tmp = [a[1], b[1]] + + newerr = tmperr { assert_equal 5, buf.__broadcast_tee(blocked, tmp, 5) } + assert_equal [a[1]], blocked + assert_match(%r{dropping}, newerr.string) + assert_equal [a[1]], tmp + end + + def test_broadcast + a = pipe + b = pipe + buf = new_buffer + buf.wr.write "HELLO" + assert_equal :wait_readable, buf.broadcast([a[1], b[1]]) + assert_equal 5, buf.bytes_xfer + assert_equal "HELLO", a[0].read(5) + assert_equal "HELLO", b[0].read(5) + assert_equal :wait_readable, buf.broadcast([a[1], b[1]]) + assert_equal 5, buf.bytes_xfer + + b[1].nonblock = true + b[1].write('*' * b[1].pipe_size) + buf.wr.write "BYE" + assert_equal :wait_readable, buf.broadcast([a[1], b[1]]) + assert_equal 8, buf.bytes_xfer + + buf.wr.write "DROP" + b[0].close + tmp = [a[1], b[1]] + newerr = tmperr { assert_equal :wait_readable, buf.broadcast(tmp) } + assert_equal 12, buf.bytes_xfer + assert_equal [a[1]], tmp + assert_match(%r{dropping}, newerr.string) + end + + def test_broadcast_total_fail + a = pipe + b = pipe + buf = new_buffer + buf.wr.write "HELLO" + a[0].close + b[0].close + tmp = [a[1], b[1]] + newerr = tmperr { assert_nil buf.broadcast(tmp) } + assert_equal [], tmp + assert_match(%r{dropping}, newerr.string) + end + + def test_broadcast_mostly_fail + a = pipe + b = pipe + c = pipe + buf = new_buffer + buf.wr.write "HELLO" + b[0].close + c[0].close + tmp = [a[1], b[1], c[1]] + newerr = tmperr { assert_equal :wait_readable, buf.broadcast(tmp) } + assert_equal 5, buf.bytes_xfer + assert_equal [a[1]], tmp + assert_match(%r{dropping}, newerr.string) + end + + def test_broadcast_all_full + a = pipe + b = pipe + buf = new_buffer + a[1].write('*' * a[1].pipe_size) + b[1].write('*' * b[1].pipe_size) + + a[1].nonblock = true + b[1].nonblock = true + tmp = [a[1], b[1]] + + buf.wr.write "HELLO" + assert_equal tmp, buf.broadcast(tmp) + assert_equal [a[1], b[1]], tmp + end + + def test_serialize + buf = new_buffer + hash = buf.to_hsh + assert_empty hash + buf.buffer_size = 4096 + hash = buf.to_hsh + assert_equal %w(buffer_size), hash.keys + assert_kind_of Integer, hash["buffer_size"] + assert_operator hash["buffer_size"], :>, 0 + end + + def test_close + buf = DTAS::Buffer.new + buf.wr.write "HI" + assert_equal 2, buf.inflight + buf.close + assert_equal 0, buf.inflight + assert_nil buf.close! + end + + def test_load_nil + buf = DTAS::Buffer.load(nil) + buf.close! + end + + def test_load_empty + buf = DTAS::Buffer.load({}) + buf.close! + end + + def test_load_size + buf = DTAS::Buffer.load({"buffer_size" => 4096}) + assert_equal 4096, buf.buffer_size + buf.close! + end +end diff --git a/test/test_format.rb b/test/test_format.rb new file mode 100644 index 0000000..ba039a7 --- /dev/null +++ b/test/test_format.rb @@ -0,0 +1,61 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'tempfile' +require 'dtas/format' + +class TestFormat < Minitest::Unit::TestCase + def test_initialize + fmt = DTAS::Format.new + assert_equal %w(-ts32 -c2 -r44100), fmt.to_sox_arg + hash = fmt.to_hsh + assert_equal({}, hash) + end + + def test_nonstandard + fmt = DTAS::Format.new + fmt.type = "s16" + fmt.rate = 48000 + fmt.channels = 4 + hash = fmt.to_hsh + assert_kind_of Hash, hash + assert_equal %w(channels rate type), hash.keys.sort + assert_equal "s16", hash["type"] + assert_equal 48000, hash["rate"] + assert_equal 4, hash["channels"] + + # back to stereo + fmt.channels = 2 + hash = fmt.to_hsh + assert_equal %w(rate type), hash.keys.sort + assert_equal "s16", hash["type"] + assert_equal 48000, hash["rate"] + assert_nil hash["channels"] + end + + def test_from_file + Tempfile.open(%w(tmp .wav)) do |tmp| + # generate an empty file with 1s of audio + cmd = %W(sox -r 96000 -b 24 -c 2 -n #{tmp.path} trim 0 1) + system(*cmd) + assert $?.success?, "#{cmd.inspect} failed: #$?" + fmt = DTAS::Format.new + fmt.from_file tmp.path + assert_equal 96000, fmt.rate + assert_equal 2, fmt.channels + tmp.unlink + end + end + + def test_bytes_per_sample + fmt = DTAS::Format.new + assert_equal 4, fmt.bytes_per_sample + fmt.type = "f64" + assert_equal 8, fmt.bytes_per_sample + fmt.type = "f32" + assert_equal 4, fmt.bytes_per_sample + fmt.type = "s16" + assert_equal 2, fmt.bytes_per_sample + end +end diff --git a/test/test_format_change.rb b/test/test_format_change.rb new file mode 100644 index 0000000..ed0e7a2 --- /dev/null +++ b/test/test_format_change.rb @@ -0,0 +1,49 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/player_integration' +require 'tmpdir' +class TestFormatChange < Minitest::Unit::TestCase + include PlayerIntegration + + def test_format_change + s = client_socket + default_pid = default_sink_pid(s) + Dir.mktmpdir do |dir| + d = "#{dir}/dump.$CHANNELS.$RATE" + f44100 = File.open("#{dir}/dump.2.44100", IO::RDWR|IO::CREAT) + f88200 = File.open("#{dir}/dump.2.88200", IO::RDWR|IO::CREAT) + s.preq("sink ed dump active=true command='cat > #{d}'") + assert_equal "OK", s.readpartial(666) + noise, len = tmp_noise + s.preq(%W(enq #{noise.path})) + assert_equal "OK", s.readpartial(666) + wait_files_not_empty(default_pid, f44100) + + s.preq("format rate=88200") + assert_equal "OK", s.readpartial(666) + + wait_files_not_empty(f88200) + + dethrottle_decoder(s) + + Timeout.timeout(len) do + begin + s.preq("current") + cur = YAML.load(s.readpartial(6666)) + end while cur["sinks"] && sleep(0.01) + end + + c = "sox -R -ts32 -c2 -r88200 #{dir}/dump.2.88200 " \ + "-ts32 -c2 -r44100 #{dir}/part2" + assert(system(c), c) + + c = "sox -R -ts32 -c2 -r44100 #{dir}/dump.2.44100 " \ + "-ts32 -c2 -r44100 #{dir}/part2 #{dir}/res.sox" + assert(system(c), c) + + assert_equal `soxi -s #{dir}/res.sox`, `soxi -s #{noise.path}` + File.unlink(*Dir["#{dir}/*"].to_a) + end + end +end diff --git a/test/test_player.rb b/test/test_player.rb new file mode 100644 index 0000000..18a8a9e --- /dev/null +++ b/test/test_player.rb @@ -0,0 +1,37 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'tempfile' +require 'dtas/player' + +class TestPlayer < Minitest::Unit::TestCase + def setup + @player = nil + tmp = Tempfile.new(%w(dtas-player-test .sock)) + @path = tmp.path + File.unlink(@path) + end + + def teardown + @player.close if @player + end + + def test_player_new + player = DTAS::Player.new + player.socket = @path + player.bind + assert File.socket?(@path) + ensure + player.close + refute File.socket?(@path) + end + + def test_player_serialize + @player = DTAS::Player.new + @player.socket = @path + @player.bind + hash = @player.to_hsh + assert_equal({"socket" => @path}, hash) + end +end diff --git a/test/test_player_client_handler.rb b/test/test_player_client_handler.rb new file mode 100644 index 0000000..9324101 --- /dev/null +++ b/test/test_player_client_handler.rb @@ -0,0 +1,86 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'dtas/player' + +class TestPlayerClientHandler < Minitest::Unit::TestCase + class MockIO < Array + alias emit push + end + + include DTAS::Player::ClientHandler + + def setup + @sinks = {} + @io = MockIO.new + @srv = nil # unused mock + end + + def test_delete + @sinks["default"] = DTAS::Sink.new + @targets = [] + sink_handler(@io, %w(rm default)) + assert @sinks.empty? + assert_equal %w(OK), @io.to_a + end + + def test_delete_noexist + sink_handler(@io, %w(rm default)) + assert @sinks.empty? + assert_equal ["ERR default not found"], @io.to_a + end + + def test_env + sink_handler(@io, %w(ed default env.FOO=bar)) + assert_equal "bar", @sinks["default"].env["FOO"] + sink_handler(@io, %w(ed default env.FOO=)) + assert_equal "", @sinks["default"].env["FOO"] + sink_handler(@io, %w(ed default env#FOO)) + assert_nil @sinks["default"].env["FOO"] + end + + def test_sink_ed + command = 'sox -t $SOX_FILETYPE -r $RATE -c $CHANNELS - \ + -t s$SINK_BITS -r $SINK_RATE -c $SINK_CHANNELS - | \ + aplay -D hw:DAC_1 -v -q -M --buffer-size=500000 --period-size=500 \ + --disable-softvol --start-delay=100 \ + --disable-format --disable-resample --disable-channels \ + -t raw -c $SINK_CHANNELS -f S${SINK_BITS}_3LE -r $SINK_RATE + ' + sink_handler(@io, %W(ed foo command=#{command})) + assert_equal command, @sinks["foo"].command + assert_empty @sinks["foo"].env + sink_handler(@io, %W(ed foo env.SINK_BITS=24)) + sink_handler(@io, %W(ed foo env.SINK_CHANNELS=2)) + sink_handler(@io, %W(ed foo env.SINK_RATE=48000)) + expect = { + "SINK_BITS" => "24", + "SINK_CHANNELS" => "2", + "SINK_RATE" => "48000", + } + assert_equal expect, @sinks["foo"].env + @io.all? { |s| assert_equal "OK", s } + assert_equal 4, @io.size + end + + def test_cat + sink = DTAS::Sink.new + sink.name = "default" + sink.command += "dither -s" + @sinks["default"] = sink + sink_handler(@io, %W(cat default)) + assert_equal 1, @io.size + hsh = YAML.load(@io[0]) + assert_kind_of Hash, hsh + assert_equal "default", hsh["name"] + assert_match("dither -s", hsh["command"]) + end + + def test_ls + expect = %w(a b c d) + expect.each { |s| @sinks[s] = true } + sink_handler(@io, %W(ls)) + assert_equal expect, Shellwords.split(@io[0]) + end +end diff --git a/test/test_player_integration.rb b/test/test_player_integration.rb new file mode 100644 index 0000000..f7b1306 --- /dev/null +++ b/test/test_player_integration.rb @@ -0,0 +1,199 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/player_integration' +class TestPlayerIntegration < Minitest::Unit::TestCase + include PlayerIntegration + + def test_cmd_rate + pid = fork do + @fmt.to_env.each { |k,v| ENV[k] = v } + exec("sox -n $SOXFMT - synth 3 pinknoise | #@cmd") + end + t = Time.now + _, _ = Process.waitpid2(pid) + elapsed = Time.now - t + assert_in_delta 3.0, elapsed, 0.5 + end if ENV["MATH_IS_HARD"] # ensure our @cmd timing is accurate + + def test_sink_close_after_play + s = client_socket + @cmd = "cat >/dev/null" + default_pid = default_sink_pid(s) + Tempfile.open('junk') do |junk| + pink = "sox -n $SOXFMT - synth 0.0001 pinknoise | tee -i #{junk.path}" + s.send("enq-cmd \"#{pink}\"", Socket::MSG_EOR) + wait_files_not_empty(junk) + assert_equal "OK", s.readpartial(666) + end + wait_files_not_empty(default_pid) + pid = read_pid_file(default_pid) + wait_pid_dead(pid) + end + + def test_sink_killed_during_play + s = client_socket + default_pid = default_sink_pid(s) + cmd = Tempfile.new(%w(sox-cmd .pid)) + pink = "echo $$ > #{cmd.path}; sox -n $SOXFMT - synth 100 pinknoise" + s.send("enq-cmd \"#{pink}\"", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + wait_files_not_empty(cmd, default_pid) + pid = read_pid_file(default_pid) + Process.kill(:KILL, pid) + cmd_pid = read_pid_file(cmd) + wait_pid_dead(cmd_pid) + end + + def test_sink_activate + s = client_socket + s.send("sink ls", Socket::MSG_EOR) + assert_equal "default", s.readpartial(666) + + # setup two outputs + + # make the default sink trickle + default_pid = Tempfile.new(%w(dtas-test .pid)) + pf = "echo $$ >> #{default_pid.path}; " + s.send("sink ed default command='#{pf}#@cmd'", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + + # make a sleepy sink trickle, too + sleepy_pid = Tempfile.new(%w(dtas-test .pid)) + pf = "echo $$ >> #{sleepy_pid.path};" + s.send("sink ed sleepy command='#{pf}#@cmd' active=true", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + + # ensure both sinks were created + s.send("sink ls", Socket::MSG_EOR) + assert_equal "default sleepy", s.readpartial(666) + + # generate pinknoise + pinknoise = "sox -n -r 44100 -c 2 -t s32 - synth 0 pinknoise" + s.send("enq-cmd \"#{pinknoise}\"", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + + # wait for sinks to start + wait_files_not_empty(sleepy_pid, default_pid) + + # deactivate sleepy sink and ensure it's gone + sleepy = File.read(sleepy_pid).to_i + assert_operator sleepy, :>, 0 + Process.kill(0, sleepy) + s.send("sink ed sleepy active=false", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + wait_pid_dead(sleepy) + + # ensure default sink is still alive + default = File.read(default_pid).to_i + assert_operator default, :>, 0 + Process.kill(0, default) + + # restart sleepy sink + sleepy_pid.sync = true + sleepy_pid.seek(0) + sleepy_pid.truncate(0) + s.send("sink ed sleepy active=true", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + + # wait for sleepy sink + wait_files_not_empty(sleepy_pid) + + # check sleepy restarted + sleepy = File.read(sleepy_pid).to_i + assert_operator sleepy, :>, 0 + Process.kill(0, sleepy) + + # stop playing current track + s.send("skip", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + + wait_pid_dead(sleepy) + wait_pid_dead(default) + end + + def test_env_change + s = client_socket + tmp = Tempfile.new(%w(env .txt)) + s.preq("sink ed default active=true command='cat >/dev/null'") + assert_equal "OK", s.readpartial(666) + + s.preq("env FOO=BAR") + assert_equal "OK", s.readpartial(666) + s.preq(["enq-cmd", "echo $FOO | tee #{tmp.path}"]) + assert_equal "OK", s.readpartial(666) + wait_files_not_empty(tmp) + assert_equal "BAR\n", tmp.read + + tmp.rewind + tmp.truncate(0) + s.preq("env FOO#") + assert_equal "OK", s.readpartial(666) + s.preq(["enq-cmd", "echo -$FOO- | tee #{tmp.path}"]) + assert_equal "OK", s.readpartial(666) + wait_files_not_empty(tmp) + assert_equal "--\n", tmp.read + end + + def test_sink_env + s = client_socket + tmp = Tempfile.new(%w(env .txt)) + s.preq("sink ed default active=true command='echo -$FOO- > #{tmp.path}'") + assert_equal "OK", s.readpartial(666) + + s.preq("sink ed default env.FOO=BAR") + assert_equal "OK", s.readpartial(666) + s.preq(["enq-cmd", "echo HI"]) + assert_equal "OK", s.readpartial(666) + wait_files_not_empty(tmp) + assert_equal "-BAR-\n", tmp.read + + tmp.rewind + tmp.truncate(0) + s.preq("sink ed default env#FOO") + assert_equal "OK", s.readpartial(666) + + Timeout.timeout(5) do + begin + s.preq("current") + yaml = s.readpartial(66666) + cur = YAML.load(yaml) + end while cur["sinks"] && sleep(0.01) + end + + s.preq(["enq-cmd", "echo HI"]) + assert_equal "OK", s.readpartial(666) + wait_files_not_empty(tmp) + assert_equal "--\n", tmp.read + end + + def test_enq_head + s = client_socket + default_sink_pid(s) + dump = Tempfile.new(%W(d .sox)) + s.preq "sink ed dump active=true command='sox $SOXFMT - #{dump.path}'" + assert_equal "OK", s.readpartial(666) + noise, len = tmp_noise + s.preq("enq-head #{noise.path}") + assert_equal "OK", s.readpartial(666) + s.preq("enq-head #{noise.path} 4") + assert_equal "OK", s.readpartial(666) + s.preq("enq-head #{noise.path} 3") + assert_equal "OK", s.readpartial(666) + dethrottle_decoder(s) + expect = Tempfile.new(%W(expect .sox)) + + c = "sox #{noise.path} -t sox '|sox #{noise.path} -p trim 3' " \ + "-t sox '|sox #{noise.path} -p trim 4' #{expect.path}" + assert system(c) + Timeout.timeout(len) do + begin + s.preq("current") + yaml = s.readpartial(66666) + cur = YAML.load(yaml) + end while cur["sinks"] && sleep(0.01) + end + assert(system("cmp", dump.path, expect.path), + "files don't match #{dump.path} != #{expect.path}") + end +end diff --git a/test/test_rg_integration.rb b/test/test_rg_integration.rb new file mode 100644 index 0000000..d8a8b85 --- /dev/null +++ b/test/test_rg_integration.rb @@ -0,0 +1,117 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/player_integration' +class TestRgIntegration < Minitest::Unit::TestCase + include PlayerIntegration + + def tmp_pluck(len = 5) + pluck = Tempfile.open(%w(pluck .flac)) + cmd = %W(sox -R -n -r44100 -c2 -C0 #{pluck.path} synth #{len} pluck) + assert system(*cmd), cmd + cmd = %W(metaflac + --set-tag=REPLAYGAIN_TRACK_GAIN=-2 + --set-tag=REPLAYGAIN_ALBUM_GAIN=-3.0 + --set-tag=REPLAYGAIN_TRACK_PEAK=0.666 + --set-tag=REPLAYGAIN_ALBUM_PEAK=0.999 + #{pluck.path}) + assert system(*cmd), cmd + [ pluck, len ] + end + + def test_rg_changes_added + s = client_socket + pluck, len = tmp_pluck + + # create the default sink, as well as a dumper + dumper = Tempfile.open(%w(dump .sox)) + dump_pid = Tempfile.new(%w(dump .pid)) + default_pid = default_sink_pid(s) + dump_cmd = "echo $$ > #{dump_pid.path}; sox $SOXFMT - #{dumper.path}" + s.send("sink ed dump active=true command='#{dump_cmd}'", Socket::MSG_EOR) + assert_equal("OK", s.readpartial(666)) + + # start playback! + s.send("enq \"#{pluck.path}\"", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + + # wait for playback to start + yaml = cur = nil + Timeout.timeout(5) do + begin + s.send("current", Socket::MSG_EOR) + cur = YAML.load(yaml = s.readpartial(1666)) + end while cur["current_offset"] == 0 && sleep(0.01) + end + + assert_nil cur["current"]["env"]["RGFX"] + + assert_equal DTAS::Format.new.rate * len, cur["current_expect"] + + wait_files_not_empty(dump_pid) + pid = read_pid_file(dump_pid) + + check_gain = proc do |expect, mode| + s.send("rg mode=#{mode}", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + Timeout.timeout(5) do + begin + s.send("current", Socket::MSG_EOR) + cur = YAML.load(yaml = s.readpartial(3666)) + end while cur["current"]["env"]["RGFX"] !~ expect && sleep(0.01) + end + assert_match expect, cur["current"]["env"]["RGFX"] + end + + check_gain.call(%r{vol -3dB}, "album_gain") + check_gain.call(%r{vol -2dB}, "track_gain") + check_gain.call(%r{vol 1\.3}, "track_peak") + check_gain.call(%r{vol 1\.0}, "album_peak") + + s.send("rg preamp+=1", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + s.send("rg", Socket::MSG_EOR) + rg = YAML.load(yaml = s.readpartial(3666)) + assert_equal 1, rg["preamp"] + + s.send("rg preamp-=1", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + s.send("rg", Socket::MSG_EOR) + rg = YAML.load(yaml = s.readpartial(3666)) + assert_nil rg["preamp"] + + s.send("rg preamp=2", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + s.send("rg", Socket::MSG_EOR) + rg = YAML.load(yaml = s.readpartial(3666)) + assert_equal 2, rg["preamp"] + + s.send("rg preamp-=0.3", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + s.send("rg", Socket::MSG_EOR) + rg = YAML.load(yaml = s.readpartial(3666)) + assert_equal 1.7, rg["preamp"] + + s.send("rg preamp-=-0.3", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + s.send("rg", Socket::MSG_EOR) + rg = YAML.load(yaml = s.readpartial(3666)) + assert_equal 2.0, rg["preamp"] + + s.send("rg preamp-=+0.3", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + s.send("rg", Socket::MSG_EOR) + rg = YAML.load(yaml = s.readpartial(3666)) + assert_equal 1.7, rg["preamp"] + + dethrottle_decoder(s) + + # ensure we did not change audio length + wait_pid_dead(pid, len) + samples = `soxi -s #{dumper.path}`.to_i + assert_equal cur["current_expect"], samples + assert_equal `soxi -d #{dumper.path}`, `soxi -d #{pluck.path}` + + stop_playback(default_pid, s) + end +end diff --git a/test/test_rg_state.rb b/test/test_rg_state.rb new file mode 100644 index 0000000..f591ac8 --- /dev/null +++ b/test/test_rg_state.rb @@ -0,0 +1,32 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'dtas/rg_state' + +class TestRGState < Minitest::Unit::TestCase + + def test_rg_state + rg = DTAS::RGState.new + assert_equal({}, rg.to_hsh) + rg.preamp = 0.1 + assert_equal({"preamp" => 0.1}, rg.to_hsh) + rg.preamp = 0 + assert_equal({}, rg.to_hsh) + end + + def test_load + rg = DTAS::RGState.load("preamp" => 0.666) + assert_equal({"preamp" => 0.666}, rg.to_hsh) + end + + def test_mode_set + rg = DTAS::RGState.new + orig = rg.mode + assert_equal DTAS::RGState::RG_DEFAULT["mode"], orig + %w(album_gain track_gain album_peak track_peak).each do |t| + rg.mode = t + assert_equal t, rg.mode + end + end +end diff --git a/test/test_sink.rb b/test/test_sink.rb new file mode 100644 index 0000000..959bbf0 --- /dev/null +++ b/test/test_sink.rb @@ -0,0 +1,32 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'dtas/sink' +require 'yaml' + +class TestSink < Minitest::Unit::TestCase + def test_serialize_reload + sink = DTAS::Sink.new + sink.name = "DAC" + hash = sink.to_hsh + assert_kind_of Hash, hash + refute_match(%r{ruby}i, hash.to_yaml, "ruby guts exposed: #{hash}") + + s2 = DTAS::Sink.load(hash) + assert_equal sink.to_hsh, s2.to_hsh + assert_equal hash, s2.to_hsh + end + + def test_name + sink = DTAS::Sink.new + sink.name = "dac1" + assert_equal({"name" => "dac1"}, sink.to_hsh) + end + + def test_inactive_load + orig = { "active" => false }.freeze + tmp = orig.to_yaml + assert_equal orig, YAML.load(tmp) + end +end diff --git a/test/test_sink_reader_play.rb b/test/test_sink_reader_play.rb new file mode 100644 index 0000000..03aa979 --- /dev/null +++ b/test/test_sink_reader_play.rb @@ -0,0 +1,49 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'dtas/sink_reader_play' +require './test/helper' + +class TestSinkReaderPlay < Minitest::Unit::TestCase + FMT = "\rIn:%-5s %s [%s] Out:%-5s [%6s|%-6s] %s Clip:%-5s" + ZERO = "\rIn:0.00% 00:00:00.00 [00:00:00.00] Out:0 " \ + "[ | ] Clip:0 " + + def setup + @srp = DTAS::SinkReaderPlay.new + end + + def teardown + @srp.close + end + + def test_sink_reader_play + @srp.wr.write(ZERO) + assert_equal :wait_readable, @srp.readable_iter + assert_equal "0", @srp.clips + assert_equal nil, @srp.headroom + assert_equal "[ | ]", @srp.meter + assert_equal "0", @srp.out + assert_equal "00:00:00.00", @srp.time + + noheadroom = sprintf(FMT, '0.00%', '00:00:37.34', '00:00:00.00', + '1.65M', ' -====', '==== ', ' ' * 6, '3M') + @srp.wr.write(noheadroom) + assert_equal :wait_readable, @srp.readable_iter + assert_equal '3M', @srp.clips + assert_equal nil, @srp.headroom + assert_equal '[ -====|==== ]', @srp.meter + assert_equal '1.65M', @srp.out + assert_equal '00:00:37.34', @srp.time + + headroom = sprintf(FMT, '0.00%', '00:00:37.43', '00:00:00.00', + '1.66M', ' =====', '===== ', 'Hd:1.2', '3.1M') + @srp.wr.write(headroom) + assert_equal :wait_readable, @srp.readable_iter + assert_equal '3.1M', @srp.clips + assert_equal '1.2', @srp.headroom + assert_equal '[ =====|===== ]', @srp.meter + assert_equal '1.66M', @srp.out + assert_equal '00:00:37.43', @srp.time + end +end diff --git a/test/test_sink_tee_integration.rb b/test/test_sink_tee_integration.rb new file mode 100644 index 0000000..8c20adb --- /dev/null +++ b/test/test_sink_tee_integration.rb @@ -0,0 +1,34 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/player_integration' +class TestSinkTeeIntegration < Minitest::Unit::TestCase + include PlayerIntegration + + def test_tee_integration + s = client_socket + default_sink_pid(s) + tee_pid = Tempfile.new(%w(dtas-test .pid)) + orig = Tempfile.new(%w(orig .junk)) + ajunk = Tempfile.new(%w(a .junk)) + bjunk = Tempfile.new(%w(b .junk)) + cmd = "echo $$ > #{tee_pid.path}; " \ + "cat /dev/fd/a > #{ajunk.path} & " \ + "cat /dev/fd/b > #{bjunk.path}; wait" + s.send("sink ed split active=true command='#{cmd}'", Socket::MSG_EOR) + assert_equal("OK", s.readpartial(666)) + pluck = "sox -n $SOXFMT - synth 3 pluck | tee #{orig.path}" + s.send("enq-cmd \"#{pluck}\"", Socket::MSG_EOR) + assert_equal "OK", s.readpartial(666) + + wait_files_not_empty(tee_pid) + pid = read_pid_file(tee_pid) + dethrottle_decoder(s) + wait_pid_dead(pid) + assert_equal ajunk.size, bjunk.size + assert_equal orig.size, bjunk.size + assert_equal ajunk.read, bjunk.read + bjunk.rewind + assert_equal orig.read, bjunk.read + end +end diff --git a/test/test_source.rb b/test/test_source.rb new file mode 100644 index 0000000..21a56ac --- /dev/null +++ b/test/test_source.rb @@ -0,0 +1,102 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'dtas/source' +require 'tempfile' + +class TestSource < Minitest::Unit::TestCase + def teardown + @tempfiles.each { |tmp| tmp.close! } + end + + def setup + @tempfiles = [] + end + + def x(cmd) + system(*cmd) + assert $?.success?, cmd.inspect + end + + def new_file(suffix) + tmp = Tempfile.new(%W(tmp .#{suffix})) + @tempfiles << tmp + cmd = %W(sox -r 44100 -b 16 -c 2 -n #{tmp.path} trim 0 1) + return tmp if system(*cmd) + nil + end + + def test_flac + return if `which metaflac`.strip.size == 0 + tmp = new_file('flac') or return + + source = DTAS::Source.new(tmp.path) + x(%W(metaflac --set-tag=FOO=BAR #{tmp.path})) + x(%W(metaflac --add-replay-gain #{tmp.path})) + assert_equal source.comments["FOO"], "BAR" + rg = source.replaygain + assert_kind_of DTAS::ReplayGain, rg + assert_in_delta 0.0, rg.track_peak.to_f, 0.00000001 + assert_in_delta 0.0, rg.album_peak.to_f, 0.00000001 + assert_operator rg.album_gain.to_f, :>, 1 + assert_operator rg.track_gain.to_f, :>, 1 + end + + def test_mp3gain + return if `which mp3gain`.strip.size == 0 + a = new_file('mp3') or return + b = new_file('mp3') or return + + source = DTAS::Source.new(a.path) + + # redirect stdout to /dev/null temporarily, mp3gain is noisy + File.open("/dev/null", "w") do |null| + old_out = $stdout.dup + $stdout.reopen(null) + begin + x(%W(mp3gain -q #{a.path} #{b.path})) + ensure + $stdout.reopen(old_out) + old_out.close + end + end + + rg = source.replaygain + assert_kind_of DTAS::ReplayGain, rg + assert_in_delta 0.0, rg.track_peak.to_f, 0.00000001 + assert_in_delta 0.0, rg.album_peak.to_f, 0.00000001 + assert_operator rg.album_gain.to_f, :>, 1 + assert_operator rg.track_gain.to_f, :>, 1 + end + + def test_offset + tmp = new_file('sox') or return + source = DTAS::Source.new(*%W(#{tmp.path} 5s)) + assert_equal 5, source.offset_samples + + source = DTAS::Source.new(*%W(#{tmp.path} 1:00:00.5)) + expect = 1 * 60 * 60 * 44100 + (44100/2) + assert_equal expect, source.offset_samples + + source = DTAS::Source.new(*%W(#{tmp.path} 1:10.5)) + expect = 1 * 60 * 44100 + (10 * 44100) + (44100/2) + assert_equal expect, source.offset_samples + + source = DTAS::Source.new(*%W(#{tmp.path} 10.03)) + expect = (10 * 44100) + (44100 * 3/100.0) + assert_equal expect, source.offset_samples + end + + def test_offset_us + tmp = new_file('sox') or return + source = DTAS::Source.new(*%W(#{tmp.path} 441s)) + assert_equal 10000.0, source.offset_us + + source = DTAS::Source.new(*%W(#{tmp.path} 22050s)) + assert_equal 500000.0, source.offset_us + + source = DTAS::Source.new(tmp.path, '1') + assert_equal 1000000.0, source.offset_us + end +end diff --git a/test/test_unixserver.rb b/test/test_unixserver.rb new file mode 100644 index 0000000..46ddb49 --- /dev/null +++ b/test/test_unixserver.rb @@ -0,0 +1,66 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'tempfile' +require 'dtas/unix_server' +require 'stringio' + +class TestUNIXServer < Minitest::Unit::TestCase + def setup + @tmp = Tempfile.new(%w(dtas-unix_server-test .sock)) + File.unlink(@tmp.path) + @clients = [] + @srv = DTAS::UNIXServer.new(@tmp.path) + end + + def test_close + assert File.exist?(@tmp.path) + assert_nil @srv.close + refute File.exist?(@tmp.path) + end + + def teardown + @clients.each { |io| io.close unless io.closed? } + if File.exist?(@tmp.path) + @tmp.close! + else + @tmp.close + end + end + + def new_client + c = Socket.new(:AF_UNIX, :SEQPACKET, 0) + @clients << c + c.connect(Socket.pack_sockaddr_un(@tmp.path)) + c + end + + def test_server_loop + client = new_client + @srv.run_once # nothing + msgs = [] + clients = [] + client.send("HELLO", Socket::MSG_EOR) + @srv.run_once do |c, msg| + clients << c + msgs << msg + end + assert_equal %w(HELLO), msgs, clients.inspect + assert_equal 1, clients.size + c = clients[0] + c.emit "HIHI" + assert_equal "HIHI", client.recv(4) + + err = nil + 500.times do + rc = c.emit "SPAM" + case rc + when StandardError + err = rc + break + end + end + assert_kind_of RuntimeError, err + end +end diff --git a/test/test_util.rb b/test/test_util.rb new file mode 100644 index 0000000..9c0e218 --- /dev/null +++ b/test/test_util.rb @@ -0,0 +1,15 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require './test/helper' +require 'dtas/util' + +class TestUtil < Minitest::Unit::TestCase + include DTAS::Util + def test_util + orig = 6.0 + lin = db_to_linear(orig) + db = linear_to_db(lin) + assert_in_delta orig, db, 0.00000001 + end +end |