# frozen_string_literal: true require_relative 'test_helper' ENV['EDITOR'] = 'true' # a test must never open a real editor require 'pty' require_relative '../lib/rdb' class SelectionTest < RdbCase def test_column_d_deletes_columns out = rdb('column', '-d', 'Cost', 'Value', file: 'inventory') assert_equal %w[Item Amount Description], out.lines.first.chomp.split("\t") assert_equal %w[1 3 rubber\ gloves], data(out).first assert_equal %w[Date Debit Credit], rdb('column', '-d', 'Account', 'Description', stdin: rdb('subtotal', 'Date', 'Debit', 'Credit', file: 'journal')).lines.first.chomp.split("\t") end def test_column_d_errors src = File.read(File.join(@dir, 'owners')) [%w[-d], %w[-d Nope], %w[-d Item Owner]].each do |args| out, err, st = run3('column', *args, stdin: src) refute st.success?, "column #{args.join(' ')} should fail" assert_equal '', out refute_empty err end end def test_column_d_works_on_lists_and_with_icase list = rdb('tabletolist', file: 'owners') assert_includes rdb('column', '-d', 'Item', stdin: list), "Owner\tann" out, _e, st = run3('column', '-d', 'COST', stdin: File.read(File.join(@dir, 'inventory')), env: { 'RDB_ICASE' => '1' }) assert st.success? refute_includes out.lines.first, 'Cost' end def test_splittable_pieces_and_errors out, = run3('splittable', '-4', 'inventory') assert_equal 4, data(File.read(File.join(@dir, 'inventoryaa'))).size assert_equal 3, data(File.read(File.join(@dir, 'inventoryab'))).size assert_includes File.read(File.join(@dir, 'inventoryab')), "Item\tAmount" _o, err, st = run3('splittable', '-0', 'inventory') refute st.success? assert_match(/usage/, err) write('big', "N\n-\n#{(1..700).to_a.join("\n")}\n") _o, err, st = run3('splittable', '-1', 'big') refute st.success? assert_match(/only 676 names/, err) assert_empty Dir[File.join(@dir, 'bigaa')] # nothing is written when it cannot all be written end def test_record_zero_and_one_are_the_first_and_negatives_count_from_the_end { '0' => '1', '1' => '1', '2' => '3', '3' => '6', '-1' => '6', '-2' => '3', '-3' => '1', '+2' => '3' }.each do |n, item| assert_equal [[item]], data(rdb('record', n, file: 'owners')).map { |r| r.first(1) }, "record #{n}" end end def test_record_beyond_either_end_is_an_empty_table %w[4 9 -4 -9].each { |n| assert_equal [], data(rdb('record', n, file: 'owners')), "record #{n}" } end def test_record_rejects_junk %w[abc 2.5 1e3].each do |n| out, err, st = run3('record', n, stdin: File.read(File.join(@dir, 'owners'))) refute st.success?, "record #{n} should fail" assert_equal '', out assert_match(/not a record number/, err) end end def test_row_numeric_condition out = rdb('row', 'Cost > 5', file: 'inventory') assert_equal %w[3 6 7], data(out).map(&:first) end def test_row_regex_and_or out = rdb('row', 'Description ~ /burner/ || Amount < 10', file: 'inventory') assert_equal %w[1 3 6 7], data(out).map(&:first) end def test_select_is_row assert_equal rdb('row', 'Cost > 5', file: 'inventory'), rdb('select', 'Cost > 5', file: 'inventory') end def test_column_reorders_and_adds_empty out = rdb('column', 'Cost', 'Nope', 'Item', file: 'inventory') assert_equal %w[Cost Nope Item], out.lines.first.chomp.split("\t") assert_equal ['5.00', '', '1'], data(out).first end def test_column_without_arguments_is_identity assert_equal read('inventory'), rdb('column', file: 'inventory') end def test_compute_sprintf out = rdb('compute', 'Value = sprintf("%5.2f", Cost * Amount)', file: 'inventory') assert_equal '1312.75', data(out)[5][3].strip end def test_sorttable_numeric_reverse out = rdb('sorttable', '-nr', 'Cost', file: 'inventory') assert_equal 'scales', data(out).first.last end def test_sorttable_two_keys out = rdb('sorttable', 'Account', 'Date', file: 'journal') assert_equal %w[101 101 101], data(out).first(3).map { |r| r[1] } end def test_record assert_equal ['3', '5', '8.00', '40.00', 'clamps'], data(rdb('record', '3', file: 'inventory')).first end def test_number out = rdb('number', file: 'inventory') assert_equal %w[number Item], out.lines.first.split("\t").first(2).map(&:chomp) assert_equal '7', data(out).last.first end def test_rename_addcol_headoff_headon assert_equal 'Qty', rdb('rename', 'Amount', 'Qty', file: 'inventory').lines.first.split("\t")[1] assert_equal 'Extra', rdb('addcol', 'Extra', file: 'inventory').lines.first.chomp.split("\t").last assert_equal 7, rdb('headoff', file: 'inventory').lines.size assert_equal %w[a b], rdb('headon', 'a', 'b', stdin: "1\t2\n").lines.first.chomp.split("\t") end end class SetsTest < RdbCase def test_union_intersect_difference assert_equal 6, data(rdb('union', 'carexpense', 'expense')).size assert_equal [%w[820113 101.62 car v car\ repairs], ['820114', '81.80', 'insur', 'c', 'car insurance allstate']], data(rdb('intersect', 'carexpense', 'expense')) assert_equal ['meal'], data(rdb('difference', 'expense', 'carexpense')).map { |r| r[2] } end def test_union_with_stdin_dash out = rdb('union', '-', 'carexpense', file: 'expense') assert_equal '820107', data(out).first.first end def test_jointable_natural_and_outer write('owners', "Item\tOwner\n----\t-----\n1\tann\n3\tbob\n9\tzed\n") inner = data(rdb('jointable', 'inventory', 'owners')) assert_equal %w[ann bob], inner.map(&:last) left = data(rdb('jointable', '-a1', 'inventory', 'owners')) assert_equal 7, left.size right = data(rdb('jointable', '-a2', 'inventory', 'owners')) assert_equal 'zed', right.last.last end def test_paste_and_split out = rdb('paste.rdb', 'expense', 'carexpense') assert_equal 10, out.lines.first.split("\t").size rdb('splittable', '-4', 'inventory') assert_equal 4, data(read('inventoryaa')).size assert_equal 3, data(read('inventoryab')).size end end class SummaryTest < RdbCase def test_total_mean_max_min assert_equal '324', data(rdb('total', 'Amount', file: 'inventory')).first[1] assert_equal '175.00', data(rdb('maximum', 'Cost', file: 'inventory')).first[2] assert_equal '0.50', data(rdb('minimum', 'Cost', file: 'inventory')).first[2] assert_equal '46.29', data(rdb('mean', 'Amount', file: 'inventory')).first[1] end def test_datatype_width_precision_widest assert_equal %w[0 0 2 2 -1], data(rdb('datatype', file: 'inventory')).first assert_equal '14', data(rdb('width', 'Description', file: 'inventory')).first.last assert_equal '2', data(rdb('precision', 'Cost', file: 'inventory')).first[2] assert_equal %w[4 6 6 7 14], data(rdb('widest', file: 'inventory')).first end def test_total_l_prints_table_too out = rdb('total', '-l', 'Amount', file: 'inventory') assert_equal 11, out.lines.size end def test_subtotal out = data(rdb('subtotal', 'Date', 'Debit', 'Credit', file: 'journal')) assert_equal [%w[861222 25000 25000], %w[861223 10000 10000], %w[861224 30000 30000]], out.map { |r| [r[0], r[2], r[3]] } end def test_schema_dbdict out = data(rdb('schema', 'inventory')) assert_equal %w[inventory 5 Description], out.last assert_equal %w[5 Description], data(rdb('dbdict', file: 'inventory')).last end def test_fd assert_equal "true\n", rdb('fd', 'Item', 'Cost', file: 'inventory') out, _e, st = run3('fd', 'Amount', 'Cost', stdin: read('inventory')) assert_equal "false\n", out refute st.success? end end class FormatTest < RdbCase def test_justify_aligns_decimals out = data(rdb('justify', file: 'inventory')) assert_equal ' 14.75', out[5][2] assert_equal '175.00', out[6][2] assert_equal ' 1', out[0][0] end def test_compresss_pad_rmblank_blank assert_equal data(read('inventory').then { |s| rdb('compresss', stdin: rdb('justify', stdin: s)) }), data(read('inventory')) assert_equal 'rubber gloves' + ' ' * 70, data(rdb('pad', file: 'inventory')).first.last t = "A\tB\n-\t-\n1\t2\n \t \n3\t4\n" assert_equal 2, data(rdb('rmblank', stdin: t)).size assert_equal [' '], data(rdb('blank', stdin: "A\n-\nab\n")).flatten end def test_trim_squeezes_columns out = rdb('trim', file: 'inventory').lines assert_equal 'Item Amount Cost Value Description ', out.first.chomp end def test_case_commands assert_equal "Rubber Gloves\n", rdb('cap', stdin: "rubber gloves\n") assert_equal "BIG WORDS\n", rdb('uppercase', 'big', 'words') assert_equal "big words\n", rdb('lowercase', 'BIG', 'WORDS') assert_equal "here is the good stuff\n", rdb('trimblank', stdin: " here is the good stuff \n") end def test_padstring_length_ascii_chr assert_equal " string\n", rdb('padstring', '10', 'string') assert_equal "string \n", rdb('padstring', '-10', 'string') assert_equal "11\n", rdb('length', 'word1 word2') assert_equal "97 65 49 \n", rdb('ascii', 'aA1') assert_equal "aA1\n", rdb('chr', '97', '65', '49') end def test_see_shows_tabs_and_ends assert_equal "a^Ib$\n", rdb('see', stdin: "a\tb\n") end def test_insertdash assert_equal "A\tBB\n-\t--\n1\t2\n", rdb('insertdash', stdin: "A\tBB\n1\t2\n") end def test_word assert_equal "I\nlove\nyou\n", rdb('word', stdin: "I love you love\n") end end class ConvertTest < RdbCase def test_list_round_trip assert_equal read('inventory'), rdb('listtotable', stdin: rdb('tabletolist', file: 'inventory')) end def test_commands_accept_lists out = rdb('column', 'Name', 'City', file: 'maillist') assert out.start_with?("\n"), 'a list in gives a list out' assert_includes out, "City\tMemphis" assert_includes rdb('row', 'Number == 2', file: 'maillist'), 'Chiquita' end def test_listtosh out = rdb('listtosh', stdin: "\nA\tx y\nB\tit's\n") assert_equal "A='x y'\nB='it'\\''s'\n", out end def test_tabletofix_and_back fixed = rdb('tabletofix', 'Item=3r', 'Description=14', file: 'inventory') assert_equal " 1rubber gloves \n", fixed.lines.first t = rdb('fixtotable', 'Item=1,3', 'Description=4,14', stdin: fixed) assert_equal ['1', 'rubber gloves'], data(t).first end def test_tabletostruct out = rdb('tabletostruct', 'Table', 'table', stdin: "A\tB\tC\n-\t-\t-\n1\t2\t3\n") assert_includes out, "struct Table\n{\nchar *A;" assert_includes out, '{ "1","2","3" }' end def test_prolog_and_translation_formats assert_equal "parent(Sally,Fred).\nparent(Mike,Fred).\nparent(Fred,Jane).\nfemale(Jane).\nfemale(Sally).\n", rdb('tabletofact', 'parent', 'female') assert_equal "mother(X,Y) :- female(X), parent(X,Y).\n", rdb('tabletorule', 'ruletable') assert_equal "define(I,Ich)\ndefine(love,liebe)\ndefine(you,dich)\n", rdb('tabletom4', file: 'ed.t') assert_equal "s/I/Ich/g\ns/love/liebe/g\ns/you/dich/g\n", rdb('tabletosed', file: 'ed.t') end def test_translate write('ed', rdb('tabletom4', file: 'ed.t')) assert_equal "Ich liebe dich.\n", rdb('translate', 'ed', stdin: "I love you.\n") end def test_tabletotbl assert_includes rdb('tabletotbl', file: 'inventory'), ".TS\nn n n n l." end def test_dates assert_equal "890515\n", rdb('computedate', '890331', '45') assert_equal "890201\n", rdb('computedate', '890132', '0') assert_equal '1752827', data(rdb('julian', 'Date', file: 'journal')).first.first assert_equal '861222', data(rdb('gregorian', 'Date', stdin: rdb('julian', 'Date', file: 'journal'))).first.first assert_equal '22/12/86', data(rdb('gregorian', '-e', 'Date', stdin: rdb('julian', 'Date', file: 'journal'))).first.first assert_match(/\A\d{6}\n\z/, rdb('todaysdate')) end def test_soundex_and_like assert_equal 'A352', data(rdb('soundex', stdin: "Name\n----\nAdams\n")).first[1] assert_equal 'R163', data(rdb('soundex', stdin: "Name\n----\nRobert\n")).first[1] write('name.x', rdb('soundex', file: 'name')) assert_equal %w[Mann Moon], data(rdb('like', 'Mann', 'name')).flatten.uniq.sort end def test_hashkey assert_equal "2\n", rdb('hashkey', '11', 'ABCD') assert_equal "4\n", rdb('hashkey', '29', 'abcdefg') end end class FastAccessTest < RdbCase # Same rows come back whichever method finds them. %w[b h i s].each do |m| define_method("test_search_method_#{m}") do rdb('index', "-m#{m}", 'inventory', 'Description') unless m == 's' out = rdb('search', "-m#{m}", 'inventory', 'Description', stdin: "scales\nclamps\n") assert_equal %w[scales clamps].sort, data(out).map(&:last).sort end end def test_record_number_method rdb('index', '-mr', 'inventory') assert_equal 'clamps', data(rdb('search', '-mr', 'inventory', stdin: "3\n")).first.last end def test_partial_match rdb('index', '-mi', '-x', 'inventory', 'Description') out = rdb('search', '-mi', '-x', 'inventory', 'Description', stdin: "cl\n") assert_equal %w[clamps cleaning\ cloth], data(out).map(&:last).sort end def test_keys_from_a_table_and_no_head out = rdb('search', '-h', 'inventory', 'Description', stdin: "Description\n-----------\nscales\n") assert_equal "7\t5\t175.00\t875.00\tscales\n", out end def test_seek_reports_offsets out = rdb('seek', 'inventory', 'Item', stdin: "5\n") from, to = out.split.map(&:to_i) assert_equal "5\t99\t2.45\t242.55\tcleaning cloth\n", read('inventory').byteslice(from, to - from) end def test_delete_blanks_in_place_and_keeps_offsets size = File.size(File.join(@dir, 'inventory')) rdb('delete', 'inventory', 'Item', stdin: "4\n") assert_equal size, File.size(File.join(@dir, 'inventory')) assert_equal '', read('inventory').lines[5].delete("\t \n") assert_equal 6, data(rdb('rmblank', file: 'inventory')).size end %w[r i h].each do |m| define_method("test_delete_updates_index_#{m}") do rdb('index', "-m#{m}", 'inventory', 'Item') rdb('delete', "-m#{m}", 'inventory', 'Item', stdin: "2\n") key = m == 'r' ? "2\n" : "2\n" assert_equal '', rdb('search', "-m#{m}", '-h', 'inventory', 'Item', stdin: key) if m != 'r' assert_equal 3, data(rdb('search', "-m#{m}", 'inventory', 'Item', stdin: "3\n")).first.first.to_i if m != 'r' end end def test_append_updates_each_index %w[i h r].each do |m| rdb('index', "-m#{m}", 'inventory', 'Item') rdb('append', '-h', "-m#{m}", 'inventory', 'Item', stdin: "#{8 + %w[i h r].index(m)}\t1\t1.00\t1.00\tnew #{m}\n") end assert_equal 'new i', data(rdb('search', '-mi', 'inventory', 'Item', stdin: "8\n")).first.last assert_equal 'new h', data(rdb('search', '-mh', 'inventory', 'Item', stdin: "9\n")).first.last assert_equal 'new r', data(rdb('search', '-mr', 'inventory', stdin: "10\n")).first.last end def test_append_with_head_line rdb('append', 'inventory', stdin: "Item\tAmount\tCost\tValue\tDescription\n----\t------\t----\t-----\t-----------\n8\t1\t1\t1\tnew\n") assert_equal 8, data(read('inventory')).size end def test_replace_in_place_pads_last_column out = rdb('seek', 'inventory', 'Item', stdin: "3\n") from, to = out.split.map(&:to_i) size = File.size(File.join(@dir, 'inventory')) rdb('replace', 'inventory', from.to_s, to.to_s, stdin: "3\t6\t8.00\t48.00\tclamp\n") assert_equal size, File.size(File.join(@dir, 'inventory')) assert_equal '6', data(rdb('search', 'inventory', 'Item', stdin: "3\n")).first[1] end def test_replace_too_big_moves_record_to_end_and_fixes_index rdb('index', '-mi', 'inventory', 'Item') from, to, xf, xt = rdb('seek', '-mi', 'inventory', 'Item', stdin: "3\n").split.map(&:to_i) big = "3\t5\t8.00\t40.00\t#{'a very long description ' * 3}\n" rdb('replace', '-mi', 'inventory', from.to_s, to.to_s, xf.to_s, xt.to_s, stdin: big) hit = rdb('search', '-mi', 'inventory', 'Item', stdin: "3\n") assert_includes hit, 'a very long description' assert_equal 1, data(hit).size assert_equal 'inventory', 'inventory' # the blanked old slot stays, so offsets of other rows are unchanged end def test_replace_with_a_no_append_flag_fails_when_too_big from, to = rdb('seek', 'inventory', 'Item', stdin: "3\n").split.map(&:to_i) _o, _e, st = run3('replace', '-a', 'inventory', from.to_s, to.to_s, stdin: "3\t5\t8\t40\t#{'x' * 80}\n") refute st.success? end def test_lock_and_unlock _o, _e, st = run3('lock', 'inventory', '111', '10', '20', '0', '0') assert st.success? _o, e, st = run3('lock', 'inventory', '222', '15', '25', '0', '0') refute st.success? assert_match(/locked by process 111/, e) assert run3('lock', 'inventory', '222', '30', '40', '0', '0').last.success? run3('unlock', 'inventory', '111', '10', '20') run3('unlock', 'inventory', '222', '30', '40') assert run3('lock', 'inventory', '333', '15', '25', '0', '0').last.success? run3('unlock', 'inventory', '333', '15', '25') end def test_lock_directory_is_private run3('lock', 'inventory', '111', '10', '20', '0', '0') dir = File.join(Dir.tmpdir, "rdb-locks-#{Process.uid}") assert_equal 0o700, File.stat(dir).mode & 0o777 run3('unlock', 'inventory', '111', '10', '20') end def test_vilock_refuses_a_table_that_is_already_locked write('LOCKinventory', 'x') _o, e, st = run3('vilock', 'inventory') refute st.success? assert_match(/is locked/, e) assert File.exist?(File.join(@dir, 'inventory')) end def test_vilock_edits_with_an_editor_that_has_arguments out, _e, st = Open3.capture3({ 'EDITOR' => 'sh -c "echo edited >> \"$0\""', 'PATH' => "#{BIN}:#{ENV['PATH']}" }, File.join(BIN, 'vilock'), 'inventory', chdir: @dir) assert st.success?, out assert_match(/edited/, File.read(File.join(@dir, 'inventory'))) refute File.exist?(File.join(@dir, 'LOCKinventory')) end def test_enter_appends_rows out = rdb('enter', '-1', 'inventory', stdin: "8\n2\n3\n4\nnew thing\n") assert_includes out, 'Item: ' assert_equal 'new thing', data(read('inventory')).last.last end end class CheckingTest < RdbCase def test_check_rdb_finds_short_rows _o, e, st = run3('check.rdb', stdin: "A\tB\n-\t-\n1\t2\n3\n") refute st.success? assert_match(/Columns \(1\) do not equal headline columns \(2\)/, e) assert run3('check.rdb', stdin: read('inventory')).last.success? end def test_domain out = rdb('domain', 'colors', stdin: rdb('column', 'Colors', file: 'orders')) assert_equal %w[farble yellow], data(out).flatten assert_equal "x\n", rdb('domain', 'colors', 'black', 'x') end def test_validate_prints_message_before_bad_row out = rdb('validate', 'Onhand < 0 {print "negative Onhand in line " NR}', file: 'badinventory') assert_includes out, "negative Onhand in line 3\n3\t-5" assert_equal 5, out.scan(/rubber|test tubes|clamps|plates|cleaning/).size end def test_searchtree write('tree', "Parent\tChild\n------\t-----\nSally\tFred\nFred\tJane\n") assert_equal "TRUE\n", rdb('searchtree', 'tree', 'Sally', 'Jane', 'Parent', 'Child') _o, _e, st = run3('searchtree', 'tree', 'Jane', 'Sally', 'Parent', 'Child') refute st.success? end def test_not assert_equal 255, run3('not', 'true').last.exitstatus assert run3('not', 'false').last.success? end end class ReportTest < RdbCase def test_report_fills_form out = rdb('report', 'form', file: 'maillist') assert_includes out, "Ronald McDonald\nMcDonalds\n123 Mac Attack\nMemphis, TENN 30000\nHi Ronald McDonald:" assert_includes out, 'Hi Chiquita Banana:' end def test_report_runs_commands_and_leaves_unknown_names write('f2', "|!>|\n") assert_equal "Ronald McDonald|Memphis\n|\nChiquita Banana|San Jose\n|\n", rdb('report', 'f2', file: 'maillist') end def test_report_does_not_run_commands_hidden_in_data write('evil', "Name\tCmd\n----\t---\nx\ttouch PWNED; echo\n") write('f3', "!>\n") out = rdb('report', 'f3', file: 'evil') assert_equal "touch PWNED; echo\n\n", out refute File.exist?(File.join(@dir, 'PWNED')) end def test_screen_rejects_unsafe_variable_names _o, e, st = run3('screen', stdin: "\n") refute st.success? assert_match(/not a valid shell variable name/, e) end def test_label assert_equal "Ronald McDonald\nMcDonalds\n123 Mac Attack\nMemphis, TENN 30000\n", rdb('label', file: 'maillist').lines.first(4).join end def test_screen_generates_a_script out = rdb('screen', stdin: "Name : \nCity : \n") assert_includes out, 'cursor 1 9 ; read Name;' assert_includes out, 'cursor 2 9 ; read City;' assert_includes out, 'echo "$Name' + "\t" + '$City"' end def test_reportwriter_pages out = rdb('reportwriter', '-3', 'Prices', file: 'inventory') assert_equal 3, out.scan('Page').size assert_includes out, 'Report Date:' end end class HelpTest < RdbCase def test_whatis_whatwill_commands assert_includes rdb('whatis', 'column'), " column - display columns of a table in any order\n column column [ -d ] [ Column ... ] < tableorlist\n" assert_includes rdb('whatwill', 'total'), 'subtotal' assert_operator data(rdb('commands')).size, :>, 120 assert_operator rdb('howmany').to_i, :>, 120 end def test_rdb_lists_commands_and_man_renders assert_includes rdb('rdb'), 'jointable' page = rdb('rdb', 'man', 'addcol') assert_includes page, 'Add a column to a table' assert_includes page, 'EXAMPLE' end def test_termput_and_cursor assert_equal "\e[H\e[2J", rdb('termput', 'cl') assert_equal "\e[3;8H", rdb('cursor', '3', '8') end def test_man_topic_pages_get_their_title assert_equal 'dash line', rdb('man', 'dashline').lines.first.chomp assert_equal 'head line', rdb('man', 'headline').lines.first.chomp assert_match(/\Arow -- Make a new table/, rdb('man', 'row')) end def test_testall_mentions_every_command script = File.read(File.join(DEMO, 'testall.cmds')) names = data(rdb('commands')).map(&:first) missing = names.reject { |n| script.match?(/(^|[\s|;(])#{Regexp.escape(n)}(\s|$)/) } assert_empty missing, "testall.cmds lacks an example for: #{missing.join(' ')}" end def test_testall_runs_demo_script out = rdb('testall') assert_includes out, '$ row' end end class ActTest < RdbCase def test_tax_foot_cashflow_explode_bom assert_equal " 3369\n", rdb('tax', '20000', file: 'X83') assert_equal %w[10001 2], data(rdb('explode', '10001', '2', 'parttable')).first assert_equal '52', data(rdb('explode', '10001', '2', 'parttable')).last.last assert_equal '1062', data(rdb('cashflow', file: 'cash'))[2][2] end def test_adjust_balance_close write('trialbalance', "Account\tDebit\tCredit\tFormat\tTaxline\tShort\tName\n-------\t-----\t------\t------\t-------\t-----\t----\n1010\t11178\t\tB-A\t4L01\tcash\tCash\n3010\t\t13500\tI-I\t101a\tsales\tSales\n") write('journaladjust', "Account\tDebit\tCredit\tFormat\tTaxline\tName\n-------\t-----\t------\t------\t-------\t----\n1010\t100\t\tB-A\t4L01\tCash\n") rdb('adjust') assert_equal %w[1010 11278], data(read('adjustedtrial')).first.first(2) rdb('balance') assert_includes read('balancesheet'), 'Total Assets' rdb('close') assert_includes read('journalnext'), 'brought forward' end def test_chartdup write('chart', "Account\tShort\n-------\t-----\n1\ta\n1\tb\n2\ta\n") _o, err, st = run3('chartdup') out, = run3('chartdup') assert_includes out, 'Duplicate Account numbers in chart' assert_includes out, 'Duplicate Short names in chart' assert st.success?, err end ICASE = { 'RDB_ICASE' => '1' }.freeze def test_column_names_are_case_sensitive_by_default _o, err, st = run3('column', 'cost', stdin: File.read(File.join(@dir, 'inventory'))) assert st.success?, err assert_equal %w[cost], _o.lines.first.chomp.split("\t") end def test_icase_column_and_sorttable out, err, st = run3('column', 'COST', 'item', stdin: File.read(File.join(@dir, 'inventory')), env: ICASE) assert st.success?, err assert_equal %w[Cost Item], out.lines.first.chomp.split("\t") out, = run3('sorttable', '-nr', 'cost', stdin: File.read(File.join(@dir, 'inventory')), env: ICASE) assert_equal 'scales', data(out).first.last end def test_icase_compute_and_awk_words src = File.read(File.join(@dir, 'inventory')) out, err, st = run3('compute', 'value = sprintf("%5.2f", cost * AMOUNT)', stdin: src, env: ICASE) assert st.success?, err assert_equal '1312.75', data(out)[5][3].strip # a column called Length must not swallow the length() function t = "Length\tName\n------\t----\n3\tab\n" out, err, st = run3('compute', 'Length = length(name)', stdin: t, env: ICASE) assert st.success?, err assert_equal [%w[2 ab]], data(out) end def test_icase_ambiguous_column_is_an_error t = "Name\tname\n----\t----\na\tb\n" _o, err, st = run3('column', 'NAME', stdin: t, env: ICASE) refute st.success? assert_match(/ambiguous/, err) out, = run3('column', 'name', stdin: t, env: ICASE) assert_equal [%w[b]], data(out) end end class MaskTest < RdbCase def form(text, frame: false) = Rdb::Mask::Form.parse(text, frame: frame) def type(session, *keys) keys.each { |k| k.start_with?("\e") || k.size == 1 ? session.feed(k) : k.each_char { |c| session.feed(c) } } end def test_parse_fields_buttons_and_width f = form("Name: < City >\n[ Save ] [Cancel]\n") assert_equal %w[Name City], f.names assert_equal [0, 6, 10], f.slots[0].to_a.first(3) assert_equal 9, f.slots[1].width # the width of "< City >" assert_equal %w[Save Cancel], f.buttons.map(&:label) assert_equal ["Name: #{' ' * 10} #{' ' * 9}", '[ Save ] [Cancel]'], f.lines end def test_command_in_form_and_frame f = form(" \n", frame: true) assert_equal '│ hi │', f.lines[1] assert_equal [1, 5], f.slots.first.to_a.first(2) end def test_typing_and_navigation s = Rdb::Mask::Session.new(form(" [ OK ] [ Cancel ]\n")) type(s, 'one', "\t", 'two', "\x7f", "\r") assert_equal({ 'A' => 'one', 'B' => 'tw' }, s.values) assert_equal :button, s.kind assert_equal :accept, s.feed("\r") end def test_cancel_by_button_and_escape s = Rdb::Mask::Session.new(form(" [ Save ] [ Abbrechen ]\n")) s.feed("\e[Z") assert_equal :cancel, s.feed(' ') assert_equal :cancel, Rdb::Mask::Session.new(form("\n")).feed("\e") end def test_enter_on_last_field_accepts_without_buttons s = Rdb::Mask::Session.new(form(" \n")) assert_nil s.feed("\r") assert_equal :accept, s.feed("\r") end def test_same_column_twice_is_one_field s = Rdb::Mask::Session.new(form(" \n")) type(s, 'xy') assert_equal({ 'A' => 'xy' }, s.values) assert_equal %w[A], form(" \n").names end def test_show_prints_picture_without_terminal write('f', "Name: \n[ Save ] [ Cancel ]\n") out = rdb('mask', '-s', file: 'f') assert_includes out, "Name: ______\n[ Save ] [ Cancel ]" assert_includes out, " Name\trow 1 col 7 width 6" assert_includes out, " [Cancel]\trow 2 col 10 (cancels)" assert_equal out, rdb('mask', '-s', 'f') # a file name works too end # Drive mask on a pseudo terminal; its table goes to a file, like in a pipe. def run_on_pty(*keys) out = File.join(@dir, 'mask.out') result = nil PTY.spawn({ 'PATH' => "#{BIN}:#{ENV['PATH']}" }, 'sh', '-c', "cd #{@dir} && mask < f > mask.out") do |r, w, pid| sleep 0.5 keys.each do |k| w.write k sleep 0.1 r.read_nonblock(65_536) rescue nil end sleep 0.3 r.read_nonblock(65_536) rescue nil _, st = Process.wait2(pid) result = [File.exist?(out) ? File.read(out) : '', st.exitstatus] end result end def test_mask_on_a_terminal_writes_a_table write('f', "Name: \nCity: \n[ Save ] [ Cancel ]\n") table, status = run_on_pty("Ann\r", "Rome\r", "\r") assert_equal 0, status assert_equal "Name\tCity\n----\t----\nAnn\tRome\n", table end def test_mask_cancel_writes_nothing write('f', "Name: \n[ Save ] [ Cancel ]\n") table, status = run_on_pty("Ann", "\e[Z", "\r") assert_equal 1, status assert_equal '', table end end class PickTest < RdbCase def session(multi: true, height: 3) t = Rdb::Table.read(File.open(File.join(DEMO, 'inventory'))) Rdb::Pick::Session.new(t.head, t.rows, multi: multi).tap { |x| x.resize(height, 80) } end def keys(sess, words) = Rdb::Pick.script(words).map { |k| sess.feed(k) }.compact.last def test_mark_and_select s = session assert_equal :accept, keys(s, 'down space down down space enter') assert_equal %w[2 5], s.selected.map(&:first) end def test_single_mode_takes_the_cursor_row_and_ignores_space s = session(multi: false) assert_equal :accept, keys(s, 'down down space enter') assert_equal %w[3], s.selected.map(&:first) end def test_scrolling_keeps_the_cursor_in_view s = session(height: 3) keys(s, 'down down down down') assert_equal [4, 2], [s.cursor, s.top] keys(s, 'end') assert_equal [6, 4], [s.cursor, s.top] keys(s, 'pgup') assert_equal 3, s.cursor keys(s, 'home') assert_equal [0, 0], [s.cursor, s.top] end def test_filter_and_clear s = session keys(s, '/clam') assert_predicate s, :typing? keys(s, 'enter') assert_equal [2], s.visible keys(s, '/ esc') assert_equal 7, s.visible.size end def test_marks_survive_a_filter_and_a_all_toggles s = session keys(s, 'space /scales enter') assert_equal [0], s.marked keys(s, 'a') assert_equal [0, 6], s.marked.sort keys(s, 'a') assert_equal [0], s.marked end def test_cancel_and_empty_choice assert_equal :cancel, keys(session, 'esc') assert_equal :cancel, keys(session, '/zzz enter enter') end def test_command_with_keys_script_is_a_filter out = rdb('pick', '-m', '-k', 'down space down down space enter', file: 'inventory') assert_equal %w[2 5], data(out).map(&:first) assert_equal '292.55', data(rdb('total', 'Value', stdin: out)).first[3] end def test_command_cancelled_exits_1_with_no_output out, _err, st = run3('pick', '-k', 'esc', stdin: File.read(File.join(@dir, 'inventory'))) refute st.success? assert_equal '', out end def test_command_reads_a_list out = rdb('pick', '-k', 'down enter', stdin: rdb('tabletolist', file: 'owners')) assert_equal [%w[3 bob]], data(out) end def run_on_pty(*key_strokes) out = File.join(@dir, 'pick.out') result = nil PTY.spawn({ 'PATH' => "#{BIN}:#{ENV['PATH']}" }, 'sh', '-c', "cd #{@dir} && pick -m < inventory > pick.out") do |r, w, pid| sleep 0.5 key_strokes.each do |k| w.write k sleep 0.1 r.read_nonblock(65_536) rescue nil end sleep 0.3 r.read_nonblock(65_536) rescue nil _, st = Process.wait2(pid) result = [File.exist?(out) ? File.read(out) : '', st.exitstatus] end result end def test_pick_on_a_terminal table, status = run_on_pty("\e[B", ' ', "\e[B", ' ', "\r") assert_equal 0, status assert_equal %w[2 4], data(table).map(&:first) end def test_pick_on_a_terminal_cancel table, status = run_on_pty("\e[B", "\e") assert_equal 1, status assert_equal '', table end end class TaxTablesTest < RdbCase def tax(income, table) = rdb('tax', income.to_s, file: table).to_i def test_at_2026 assert_equal 0, tax(13_539, 'taxAT2026') assert_equal 11_447, tax(50_000, 'taxAT2026') # 6 030.40 + 13 542 * 40 % assert_equal 33_818, tax(100_000, 'taxAT2026') end def test_ch_federal_2026 assert_equal 0, tax(15_200, 'taxCH2026') assert_equal 401, tax(50_000, 'taxCH2026') assert_equal 2684, tax(100_000, 'taxCH2026') assert_equal 91_310, tax(794_000, 'taxCH2026') end def test_li_landessteuer_2026 assert_equal 996, tax(50_000, 'taxLI2026') # 0,04 * x - 1 004 assert_equal 3256, tax(100_000, 'taxLI2026') # 0,05 * x - 1 744 assert_equal 10_464, tax(211_400, 'taxLI2026') assert_equal 0, tax(15_855, 'taxLI2026') end def test_swiss_cantons_2026_einfache_staatssteuer assert_equal 3770, tax(60_000, 'taxCH_AI2026') # 2 070 + 8,5 % of 20 000 assert_equal 16_000, tax(200_000, 'taxCH_AI2026') assert_equal 24_000, tax(300_000, 'taxCH_AI2026') # 8 % of the whole income assert_equal 1065, tax(60_000, 'taxCH_AR2026') assert_equal 3344, tax(60_000, 'taxCH_SG2026') # 1 248 + 8 % + 9,2 % pieces assert_equal 25_500, tax(300_000, 'taxCH_SG2026') # 8,5 % of the whole income assert_equal 3748, tax(60_000, 'taxCH_GR2026') assert_equal 0, tax(16_895, 'taxCH_GR2026') assert_equal 20, tax(5_000, 'taxCH_AI2026') # 1 % of 2 000 %w[AR SG GR].each { |c| assert_equal 0, tax(5_000, "taxCH_#{c}2026") } end # § 32a EStG: the table is a straight-line approximation of the curved zones def test_de_2026_follows_the_formula [12_348, 14_000, 17_799, 20_000, 33_333, 50_000, 69_878, 100_000, 277_825, 300_000].each do |x| exact = if x <= 12_348 then 0 elsif x <= 17_799 then y = (x - 12_348) / 10_000.0; ((914.51 * y) + 1400) * y elsif x <= 69_878 then z = (x - 17_799) / 10_000.0; (((173.10 * z) + 2397) * z) + 1034.87 elsif x <= 277_825 then (0.42 * x) - 11_135.63 else (0.45 * x) - 19_470.38 end assert_in_delta exact.floor, tax(x, 'taxDE2026'), 1, "income #{x}" end end end class RobustnessTest < RdbCase def test_umlauts_in_column_names write('uml', "Größe\tÜber\n-----\t----\n5\t7\n1\t2\n") assert_equal ['5', '7'], data(rdb('row', 'Über > 3', file: 'uml')).first assert_equal 1, data(rdb('row', 'Größe > 3', file: 'uml')).size end def test_latin1_data_is_read_and_written_as_utf8 File.binwrite(File.join(@dir, 'lat'), "Name\n----\n\xE4pfel\n") assert_equal "Äpfel".downcase, data(rdb('row', 'Name ~ /pfel/', file: 'lat')).first.first end def test_errors_are_one_line_not_a_backtrace _o, e, st = run3('update.rdb', 'inventory', stdin: "/[\n") refute st.success? assert_match(/update\.rdb: .*char-class/, e) refute_match(/\.rb:\d+:in/, e) end def test_enter_refuses_a_value_with_a_tab _o, e, st = run3('enter', '-1', 'inventory', stdin: "9\t9\n1\n2\n3\nthing\n") refute st.success? assert_match(/contains a tab/, e) end def test_sorttable_reverse_keeps_ties_in_order out = rdb('sorttable', '-r', 'B', stdin: "A\tB\n-\t-\n1\tb\n2\ta\n3\tb\n4\ta\n") assert_equal %w[1 3 2 4], data(out).map(&:first) end def test_update_keeps_the_record_when_the_editor_fails before = File.read(File.join(@dir, 'inventory')) _o, e, = run3('update.rdb', 'inventory', stdin: "1\ne\nq\n", env: { 'EDITOR' => 'false' }) assert_match(/editor false failed/, e) assert_equal before, File.read(File.join(@dir, 'inventory')) end def test_write_file_is_complete_and_keeps_the_mode path = File.join(@dir, 'wf') File.write(path, 'old') File.chmod(0o640, path) Rdb.write_file(path, 'new') assert_equal 'new', File.read(path) assert_equal 0o640, File.stat(path).mode & 0o777 assert_empty Dir[File.join(@dir, 'wf.tmp*')] end end class ExactnessTest < RdbCase def test_total_adds_decimals_exactly out = rdb('total', 'A', stdin: "A\n-\n0.1\n0.2\n0.7\n0.3\n") assert_equal '1.3', data(out).first.first end def test_mean_rounds_half_up out = rdb('mean', 'A', stdin: "A\n-\n0.01\n0.02\n") assert_equal '0.02', data(out).first.first end def test_subtotal_adds_decimals_exactly out = rdb('subtotal', 'K', 'A', stdin: "K\tA\n-\t-\nx\t0.1\nx\t0.2\nx\t0.7\n") assert_equal '1.0', data(out).first[1] end def test_hash_spreads_numeric_keys slots = Hash.new(0) (0...3000).each { |i| slots[Rdb::Fast.hash_slot(6000, format('%05d', i))] += 1 } assert_operator slots.size, :>, 2000 assert_operator slots.values.max, :<=, 8 end def test_indexed_search_does_not_read_all_rows rows = (1..500).map { |i| "#{format('%04d', i)}\tv#{i}\n" }.join write('big', "K\tV\n-\t-\n#{rows}") %w[h i r].each { |m| rdb('index', "-m#{m}", 'big', 'K') } data_obj = Rdb::Fast::Data.new(File.join(@dir, 'big')) %w[h i].each do |m| assert_equal ['0321', 'v321'], data(rdb('search', "-m#{m}", 'big', 'K', stdin: "0321\n")).first, "-m#{m}" end assert_equal ['0007', 'v7'], data(rdb('search', '-mr', 'big', stdin: "7\n")).first assert_nil data_obj.instance_variable_get(:@rows) assert_equal 'v500', data_obj.row_at(data_obj.rows.last.from).cells[1] end end