% Copyright (C) 2003-2019 Olivier Boudeville % % This file is part of the Ceylan-Myriad library. % % This library is free software: you can redistribute it and/or modify % it under the terms of the GNU Lesser General Public License or % the GNU General Public License, as they are published by the Free Software % Foundation, either version 3 of these Licenses, or (at your option) % any later version. % You can also redistribute it and/or modify it under the terms of the % Mozilla Public License, version 1.1 or later. % % This library is distributed in the hope that it will be useful, % but WITHOUT ANY WARRANTY; without even the implied warranty of % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the % GNU Lesser General Public License and the GNU General Public License % for more details. % % You should have received a copy of the GNU Lesser General Public % License, of the GNU General Public License and of the Mozilla Public License % along with this library. % If not, see and % . % % Authors: Jingxuan Ma [jingxuan (dot) ma (at) edf (dot) fr] % Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com] % Tests for comparing and illustrating the differences of following types of % hashtables: % % - hashtable % - tracked hashtable % - lazy hashtable % - map hashtable % - list table % % See also hashtable.erl, tracked_hashtable.erl, lazy_hashtable.erl, % map_hashtable.erl, list_table.erl and their respective test modules. % % Directly depends on the following modules: hashtable, tracked_hashtable, % lazy_hashtable, map_hashtable, list_table. % % Note that the 'table' pseudo-module is not tested here: not only it is % actually one of the previous implementations, but also modules are applied % dynamically here, hence the parse transform will not replace anything. % -module(tables_comparison_test). % For run/0 export and al: -include("test_facilities.hrl"). -define(MyFirstKey, 'MyFirstKey'). -define(MySecondKey, 'MySecondKey'). -define(MyThirdKey, 'MyThirdKey'). -define(MyFourthKey, 'MyFourthKey'). -export([ run_basic_tests/0, run_performance_tests/0 ]). -spec run() -> no_return(). run() -> test_facilities:start( ?MODULE ), %run_basic_tests(), run_performance_tests(), test_facilities:stop(). display_separator() -> test_facilities:display( " -----------------------------~n~n" ). % Performs the same set of operations of an instance of each type of hashtable. % run_basic_tests() -> % Tests for each kind of table should be separated. display_separator(), test_facilities:display( " ~n Comparison of tables creation: " ), MyH1 = hashtable:new( 10 ), true = hashtable:is_empty( MyH1 ), hashtable:display( "Vanilla hashtable", MyH1 ), MyH1Optimised = hashtable:optimise( MyH1 ), hashtable:display( "Optimized hash table", MyH1Optimised ), MyTH1 = tracked_hashtable:new( 10 ), true = tracked_hashtable:is_empty( MyTH1 ), tracked_hashtable:display( "Vanilla tracked table ", MyTH1 ), MyLH1 = lazy_hashtable:new( 10 ), true = lazy_hashtable:is_empty( MyLH1 ), lazy_hashtable:display( "Vanilla lazy hashtable", MyLH1 ), display_separator(), MyM1 = map_hashtable:new( 10 ), true = map_hashtable:is_empty( MyM1 ), map_hashtable:display( "Vanilla map hashtable", MyM1 ), display_separator(), MyL1 = list_table:new( 10 ), true = list_table:is_empty( MyL1 ), list_table:display( "Vanilla list hashtable", MyL1 ), test_facilities:display( "End of the comparison of tables creation." ), display_separator(), test_facilities:display( "Comparison of tables' state after adding entries:" ), test_facilities:display( "Adding entries in hash table:" ), MyH2 = hashtable:new( 4 ), MyH3 = hashtable:add_entry( ?MyFirstKey, "MyFirstValue", MyH2 ), false = hashtable:is_empty( MyH3 ), MyH4 = hashtable:add_entry( ?MySecondKey, "MySecondValue", MyH3 ), false = hashtable:is_empty( MyH4 ), MyH5 = hashtable:add_entry( ?MyThirdKey, [1,2,3], MyH4 ), false = hashtable:is_empty( MyH5 ), hashtable:display( MyH5 ), MyH5Optimised = hashtable:optimise( MyH5 ), hashtable:display( "Optimised hashtable", MyH5Optimised ), test_facilities:display( "Adding entries in tracked hashtable:"), MyTH2 = tracked_hashtable:new(4 ), MyTH3 = tracked_hashtable:add_entry( ?MyFirstKey, "MyFirstValue", MyTH2 ), false = tracked_hashtable:is_empty( MyTH3 ), MyTH4 = tracked_hashtable:add_entry( ?MySecondKey, "MySecondValue", MyTH3 ), false = tracked_hashtable:is_empty( MyTH4 ), MyTH5 = tracked_hashtable:add_entry( ?MyThirdKey, [1,2,3], MyTH4 ), false = tracked_hashtable:is_empty( MyTH5 ), tracked_hashtable:display( "Tracked hashtable: ", MyTH5 ), MyTH5Optimised = tracked_hashtable:optimise( MyTH5 ), tracked_hashtable:display( "Optimised tracked hashtable", MyTH5Optimised ), test_facilities:display( "Adding entries in lazy hashtable:" ), MyLH2 = lazy_hashtable:new( 4 ), MyLH3 = lazy_hashtable:add_entry( ?MyFirstKey, "MyFirstValue", MyLH2 ), false = lazy_hashtable:is_empty( MyLH3 ), MyLH4 = lazy_hashtable:add_entry( ?MySecondKey, "MySecondValue", MyLH3 ), false = lazy_hashtable:is_empty( MyLH4 ), MyLH5 = lazy_hashtable:add_entry( ?MyThirdKey, [1,2,3], MyLH4 ), false = lazy_hashtable:is_empty( MyLH5 ), lazy_hashtable:display( "Lazy hashtable: ", MyLH5 ), MyLH5Optimised = lazy_hashtable:optimise( MyLH5 ), lazy_hashtable:display( "Optimised lazy hashtable", MyLH5Optimised ), test_facilities:display( "Adding entries in map hashtable:" ), MyMH2 = map_hashtable:new( 4 ), MyMH3 = map_hashtable:add_entry( ?MyFirstKey, "MyFirstValue", MyMH2 ), false = map_hashtable:is_empty( MyMH3 ), MyMH4 = map_hashtable:add_entry( ?MySecondKey, "MySecondValue", MyMH3 ), false = map_hashtable:is_empty( MyMH4 ), MyMH5 = map_hashtable:add_entry( ?MyThirdKey, [1,2,3], MyMH4 ), false = map_hashtable:is_empty( MyMH5 ), map_hashtable:display( "Map hashtable: ", MyMH5 ), test_facilities:display( "Adding entries in list hashtable:" ), MyL2 = list_table:new( 4 ), MyL3 = list_table:add_entry( ?MyFirstKey, "MyFirstValue", MyL2 ), false = list_table:is_empty( MyL3 ), MyL4 = list_table:add_entry( ?MySecondKey, "MySecondValue", MyL3 ), false = list_table:is_empty( MyL4 ), MyL5 = list_table:add_entry( ?MyThirdKey, [1,2,3], MyL4 ), false = list_table:is_empty( MyL5 ), list_table:display( "List hashtable: ", MyL5 ), display_separator(), test_facilities:display( "Looking up for ~s in hashtable: ~p", [ ?MyFirstKey, hashtable:lookup_entry( ?MyFirstKey, MyH5 ) ] ), { value, "MyFirstValue" } = hashtable:lookup_entry( ?MyFirstKey, MyH5 ), test_facilities:display( "Removing that entry." ), MyH6 = hashtable:remove_entry( ?MyFirstKey, MyH5 ), false = hashtable:is_empty( MyH6 ), test_facilities:display( "Looking up for ~s hashtable: ~p", [ ?MyFirstKey, hashtable:lookup_entry( ?MyFirstKey, MyH6 ) ] ), key_not_found = hashtable:lookup_entry( ?MyFirstKey, MyH6 ), % remove_entry can also be used if the specified key is not here, will return % an identical table. % hashtable:display( "Hashtable", MyH6 ), test_facilities:display( "Looking up for ~s in tracked hashtable: ~p", [ ?MyFirstKey, tracked_hashtable:lookup_entry( ?MyFirstKey, MyTH5 ) ] ), { value, "MyFirstValue" } = tracked_hashtable:lookup_entry( ?MyFirstKey, MyTH5 ), test_facilities:display( "Removing that entry." ), MyTH6 = tracked_hashtable:remove_entry( ?MyFirstKey, MyTH5 ), false = tracked_hashtable:is_empty( MyTH6 ), test_facilities:display( "Looking up for ~s in tracked hashtable: ~p", [ ?MyFirstKey, tracked_hashtable:lookup_entry( ?MyFirstKey, MyTH6 ) ] ), key_not_found = tracked_hashtable:lookup_entry( ?MyFirstKey, MyTH6 ), % remove_entry can also be used if the specified key is not here, will return % an identical table. % tracked_hashtable:display( "Tracked hashtable", MyTH6 ), test_facilities:display( "Looking up for ~s in lazy hashtable: ~p", [ ?MyFirstKey, lazy_hashtable:lookup_entry( ?MyFirstKey, MyLH5 ) ] ), { value, "MyFirstValue" } = lazy_hashtable:lookup_entry( ?MyFirstKey, MyLH5 ), test_facilities:display( "Removing that entry." ), MyLH6 = lazy_hashtable:remove_entry( ?MyFirstKey, MyLH5 ), false = lazy_hashtable:is_empty( MyLH6 ), test_facilities:display( "Looking up for ~s in lazy hashtable: ~p", [ ?MyFirstKey, lazy_hashtable:lookup_entry( ?MyFirstKey, MyLH6 ) ] ), key_not_found = lazy_hashtable:lookup_entry( ?MyFirstKey, MyLH6 ), % remove_entry can also be used if the specified key is not here, will return % an identical table. % lazy_hashtable:display( "Lazy hashtable", MyLH6 ), test_facilities:display( "Looking up for ~s in map hashtable: ~p", [ ?MyFirstKey, map_hashtable:lookup_entry( ?MyFirstKey, MyMH5 ) ] ), { value, "MyFirstValue" } = map_hashtable:lookup_entry( ?MyFirstKey, MyMH5 ), test_facilities:display( "Removing that entry." ), MyMH6 = map_hashtable:remove_entry( ?MyFirstKey, MyMH5 ), false = map_hashtable:is_empty( MyMH6 ), test_facilities:display( "Looking up for ~s in map hashtable: ~p", [ ?MyFirstKey, map_hashtable:lookup_entry( ?MyFirstKey, MyMH6 ) ] ), key_not_found = map_hashtable:lookup_entry( ?MyFirstKey, MyMH6 ), test_facilities:display( "Looking up for ~s in list hashtable: ~p", [ ?MyFirstKey, list_table:lookup_entry( ?MyFirstKey, MyL5 ) ] ), { value, "MyFirstValue" } = list_table:lookup_entry( ?MyFirstKey, MyL5 ), test_facilities:display( "Removing that entry." ), MyL6 = list_table:remove_entry( ?MyFirstKey, MyL5 ), false = list_table:is_empty( MyL6 ) , test_facilities:display( "Looking up for ~s in list hashtable: ~p", [ ?MyFirstKey, list_table:lookup_entry( ?MyFirstKey, MyL6 ) ] ), key_not_found = list_table:lookup_entry( ?MyFirstKey, MyL6 ), % remove_entry can also be used if the specified key is not here, will return % an identical table. % list_table:display( "List hashtable", MyL6 ), display_separator(), test_facilities:display( "Testing double key registering." ), MyH7 = hashtable:add_entry( ?MyThirdKey, anything, MyH6 ), hashtable:display( MyH7 ), test_facilities:display( "Enumerating the hash table: ~p", [ hashtable:enumerate( MyH6 ) ] ), test_facilities:display( "Listing the hash table keys: ~p", [ hashtable:keys( MyH6 ) ] ), true = list_utils:unordered_compare( [ ?MySecondKey, ?MyThirdKey ], hashtable:keys( MyH6 ) ), MyH8 = hashtable:add_entries( [ {?MyThirdKey,3}, {?MyFourthKey,4} ], MyH7 ), MyH9 = hashtable:merge( MyH4, MyH8 ), test_facilities:display( "Merged table: ~s, size of buckets is ~B.", [ hashtable:to_string( MyH9 ), hashtable:get_bucket_count( MyH9 ) ] ), MyH10 = hashtable:optimise( MyH9 ), test_facilities:display( "The optimised table: ~s size of buckets is ~B.", [ hashtable:to_string( MyH10 ), hashtable:get_bucket_count( MyH10 ) ] ), Keys = [ ?MyFirstKey, ?MyThirdKey ], test_facilities:display( "Listing the entries for keys in table ~p:" "~n ~p", [ Keys, hashtable:select_entries( Keys, MyH10 ) ] ), MyTH7 = tracked_hashtable:add_entry( ?MyThirdKey, anything, MyTH6 ), tracked_hashtable:display( MyTH7 ), test_facilities:display( "Enumerating the tracked hash table: ~p.", [ tracked_hashtable:enumerate( MyTH6 ) ] ), test_facilities:display( "Listing the tracked table keys: ~p.", [ tracked_hashtable:keys( MyTH6 ) ] ), true = list_utils:unordered_compare( [ ?MySecondKey,?MyThirdKey ], tracked_hashtable:keys( MyTH6 ) ), MyTH8 = tracked_hashtable:add_entries( [ {?MyThirdKey,3}, {?MyFourthKey,4} ], MyTH7 ), MyTH9 = tracked_hashtable:merge( MyTH4, MyTH8 ), test_facilities:display( "Merged tracked table: ~s", [ tracked_hashtable:to_string( MyTH9 ) ] ), Keys = [ ?MyFirstKey, ?MyThirdKey ], test_facilities:display( "Listing the entries for keys ~p in tracked table:" " ~n ~p", [ Keys, tracked_hashtable:select_entries( Keys, MyTH9 ) ] ), MyLH7 = lazy_hashtable:add_entry( ?MyThirdKey, anything, MyLH6 ), lazy_hashtable:display( MyLH7 ), test_facilities:display( "Enumerating the lazy table: ~p.", [ lazy_hashtable:enumerate( MyLH6 ) ] ), test_facilities:display( "Listing the lazy table keys: ~p.", [ lazy_hashtable:keys( MyLH6 ) ] ), true = list_utils:unordered_compare( [ ?MySecondKey, ?MyThirdKey ], lazy_hashtable:keys( MyLH6 ) ), MyLH8 = lazy_hashtable:add_entries( [ {?MyThirdKey,3}, {?MyFourthKey,4} ], MyLH7 ), MyLH9 = lazy_hashtable:merge( MyLH4, MyLH8 ), test_facilities:display( "Merged lazy table: ~s", [ lazy_hashtable:to_string( MyLH9 ) ] ), Keys = [ ?MyFirstKey, ?MyThirdKey ], test_facilities:display( "Listing the entries for keys in lazy table ~p:" "~n ~p", [ Keys, lazy_hashtable:select_entries( Keys, MyLH9 ) ] ), MyMH7 = map_hashtable:add_entry( ?MyThirdKey, anything, MyMH6 ), map_hashtable:display( MyMH7 ), test_facilities:display( "Enumerating the map table: ~p.", [ map_hashtable:enumerate( MyMH6 ) ] ), test_facilities:display( "Listing the map table keys: ~p.", [ map_hashtable:keys( MyMH6 ) ] ), true = list_utils:unordered_compare( [ ?MySecondKey, ?MyThirdKey ], map_hashtable:keys( MyMH6 ) ), MyMH8 = map_hashtable:add_entries( [ {?MyThirdKey,3}, {?MyFourthKey,4} ], MyMH7 ), MyMH9 = map_hashtable:merge( MyMH4, MyMH8 ), test_facilities:display( "Merged map table: ~s", [ map_hashtable:to_string( MyMH9 ) ] ), Keys = [ ?MyFirstKey, ?MyThirdKey ], test_facilities:display( "Listing the entries for keys in map table ~p:" "~n ~p", [ Keys, map_hashtable:select_entries( Keys, MyMH9 ) ] ), MyL7 = list_table:add_entry( ?MyThirdKey, anything, MyL6 ), list_table:display( MyL7 ), test_facilities:display( "Enumerating the list table: ~p.", [ list_table:enumerate( MyL6 ) ] ), test_facilities:display( "Listing the list table keys: ~p.", [ list_table:keys( MyL6 ) ] ), true = list_utils:unordered_compare( [ ?MySecondKey, ?MyThirdKey ], list_table:keys( MyL6 ) ), MyL8 = list_table:add_entries( [ {?MyThirdKey,3}, {?MyFourthKey,4} ], MyL7 ), MyL9 = list_table:merge( MyL4, MyL8 ), test_facilities:display( "Merged list table: ~s", [ list_table:to_string( MyL9 ) ] ), Keys = [ ?MyFirstKey, ?MyThirdKey ], test_facilities:display( "Listing the entries for keys in list table ~p:" "~n ~p", [ Keys, list_table:select_entries( Keys, MyL9 ) ] ). -define( series_count, 12 ). %-define( series_count, 120 ). % Returns a suitable set of 12*7=84 (presumably the typical size of a table of % interest) key/value pairs (respectively an atom and a value of various types) % to test tables. % get_pairs() -> get_pairs( _Series=?series_count, _Count=1, _Acc=[] ). get_pairs( _Series=0, _Count, Acc ) -> Acc; get_pairs( Series, Count, Acc ) -> ToAddStrings = [ { io_lib:format( "key-~B", [ Count + 1 ] ) , self() }, { io_lib:format( "key-~B", [ Count + 2 ] ) , "hello world!" }, { io_lib:format( "key-~B", [ Count + 3 ] ) , an_atom }, { io_lib:format( "key-~B", [ Count + 4 ] ) , [ "a", 123, list ] }, { io_lib:format( "key-~B", [ Count + 5 ] ) , { 23, 45, 67, 90 } }, { io_lib:format( "key-~B", [ Count + 6 ] ) , 1.0 }, { io_lib:format( "key-~B", [ Count + 7 ] ) , << "A binary" >> } ], ToAdd = [ { text_utils:string_to_atom( lists:flatten( K ) ), V } || { K, V } <- ToAddStrings ], get_pairs( Series - 1, Count + length( ToAdd ), ToAdd ++ Acc ). % Returns different key/value pairs (the same keys associated to different % values, and as many new keys as well). % get_other_pairs( Pairs ) -> get_other_pairs( Pairs, _Acc=[] ). get_other_pairs( _Pairs=[], Acc ) -> NewPairs = get_pairs( _Series=?series_count, _Count=length( Acc ) + 1, [] ), list_utils:random_permute( Acc ++ NewPairs ); get_other_pairs( _Pairs=[ { K, _V } | T ], Acc ) -> % Same key, different value (should not matter much): NewAcc = [ { K, io_lib:format( "Value for ~p", [ K ] ) } | Acc ], get_other_pairs( T, NewAcc ). -define( run_count, 100 ). % Feeds specified table and returns the corresponding average duration, in % milliseconds (on average over Count runs). % feed_table( Table, Module, Pairs ) -> Count = ?run_count, Start = time_utils:get_precise_timestamp(), NewTables = [ lists:foldl( fun( { K, V }, T ) -> Module:add_entry( K, V, T ) end, _Acc0=Table, _List=Pairs ) || _C <- lists:seq( 1, Count ) ], Stop = time_utils:get_precise_timestamp(), Timing = time_utils:get_precise_duration( Start, Stop ) / Count, [ First | Others ] = NewTables, % Checking: [ First = OtherTable || OtherTable <- Others ], { Module, First, Timing }. % Updates specified table and returns the corresponding average duration, in % milliseconds (on average over Count runs). % update_table( Table, Module, Pairs ) -> Count = ?run_count, Start = time_utils:get_precise_timestamp(), NewTables = [ lists:foldl( fun( { K, V }, T ) -> Module:add_entry( K, V, T ) end, _Acc0=Table, _List=Pairs ) || _C <- lists:seq( 1, Count ) ], Stop = time_utils:get_precise_timestamp(), Timing = time_utils:get_precise_duration( Start, Stop ) / Count, [ First | Others ] = NewTables, % Checking: [ First = OtherTable || OtherTable <- Others ], { Module, First, Timing }. % Benchmarks look-up durations. % benchmark_look_ups( Table, Module, Pairs ) -> %test_facilities:display( "Benchmarking ~s on:~n~s", % [ Module, Module:to_string( Table ) ] ), Count = 100 * ?run_count, Start = time_utils:get_precise_timestamp(), _Values = [ lists:foldl( fun( { K, V }, T ) -> V = Module:get_value( K, T ), T end, _Acc0=Table, _List=Pairs ) || _C <- lists:seq( 1, Count ) ], Stop = time_utils:get_precise_timestamp(), Timing = time_utils:get_precise_duration( Start, Stop ) / Count, { Module, Timing }. % Runs performance tests, to compare the various table implementations. % run_performance_tests() -> Implementations = [ hashtable, tracked_hashtable, lazy_hashtable, map_hashtable, list_table ], % Change the order to see whether result variy: ActualImplementations = lists:reverse( Implementations ), EmptyTables = [ { M, M:new() } || M <- ActualImplementations ], Pairs = get_pairs(), test_facilities:display( "Feeding empty tables with ~B initial key/value pairs.", [ length( Pairs ) ] ), % Do it 5 times at blank to avoid transition effects: _FedTablesWithTimings = [ [ feed_table( T, M, Pairs ) || { M, T } <- EmptyTables ] || _C <- lists:seq( 1, 5 ) ], FedTablesWithTimings = [ feed_table( T, M, Pairs ) || { M, T } <- EmptyTables ], FedTimeStrings = [ text_utils:format( "for ~s: ~.3f ms", [ M, Timing ] ) || { M, _T, Timing } <- FedTablesWithTimings ], test_facilities:display( "~nFeed durations: ~s", [ text_utils:strings_to_string( FedTimeStrings ) ] ), FedSizeStrings = [ text_utils:format( "for ~s: ~s", [ M, system_utils:interpret_byte_size_with_unit( basic_utils:size( T ) ) ] ) || { M, T, _Timing } <- FedTablesWithTimings ], test_facilities:display( "~nSizes: ~s", [ text_utils:strings_to_string( FedSizeStrings ) ] ), OtherPairs = get_other_pairs( Pairs ), test_facilities:display( "~n~nUpdating these tables with ~B key/value pairs " "(equal mix of updated and new keys).", [ length( OtherPairs ) ] ), FedTables = [ { M, T } || { M, T, _Timing } <- FedTablesWithTimings ], UpdatedTablesWithTimings = [ update_table( T, M, OtherPairs ) || { M, T } <- FedTables ], UpTimeStrings = [ text_utils:format( "for ~s: ~.3f ms", [ M, Timing ] ) || { M, _T, Timing } <- UpdatedTablesWithTimings ], test_facilities:display( "~nUpdate durations: ~s", [ text_utils:strings_to_string( UpTimeStrings ) ] ), UpSizeStrings = [ text_utils:format( "for ~s: ~s", [ M, system_utils:interpret_byte_size_with_unit( basic_utils:size( T ) ) ] ) || { M, T, _Timing } <- UpdatedTablesWithTimings ], test_facilities:display( "~nSizes: ~s", [ text_utils:strings_to_string( UpSizeStrings ) ] ), test_facilities:display( "~nBenchmarking look-ups (with no optimisation)." ), ShuffledPairs = list_utils:random_permute( OtherPairs ), LookedUpTimings = [ benchmark_look_ups( T, M, ShuffledPairs ) || { M, T, _Timings } <- UpdatedTablesWithTimings ], LookedUpStrings = [ text_utils:format( "for ~s: ~.1f microsec", [ M, 1000 * Timing ] ) || { M, Timing } <- LookedUpTimings ], test_facilities:display( "~nLook-up durations: ~s", [ text_utils:strings_to_string( LookedUpStrings ) ] ), OptimisedTables = [ { M, M:optimise( T ), undefined } || { M, T, _Timing } <- UpdatedTablesWithTimings ], test_facilities:display( "~nBenchmarking look-ups (after optimisation)." ), NewLookedUpTimings = [ benchmark_look_ups( T, M, ShuffledPairs ) || { M, T, _Timings } <- OptimisedTables ], NewLookedUpStrings = [ text_utils:format( "for ~s: ~.1f microsec", [ M, 1000 * Timing ] ) || { M, Timing } <- NewLookedUpTimings ], test_facilities:display( "~nLook-up durations: ~s", [ text_utils:strings_to_string( NewLookedUpStrings ) ] ), FinalTablesWithTimings = UpdatedTablesWithTimings, AllListedTables = [ { _, FirstList } | Others ] = [ { M, lists:sort( M:enumerate( T ) ) } || { M, T, _Timing } <- FinalTablesWithTimings ], Hashes = [ { M, erlang:phash2( L ) } || { M, L } <- AllListedTables ], %test_facilities:display( "Checking content: ~s", % [ text_utils:strings_to_string( % [ io_lib:format( "for ~s: ~s", % [ M, M:to_string( T ) ] ) % || { M, T, _Timing } <- FinalTablesWithTimings ] ) ] ), test_facilities:display( "~nChecking hashes: ~s", [ text_utils:strings_to_string( [ io_lib:format( "for ~s: ~p", [ M, H ] ) || { M, H } <- Hashes ] ) ] ), test_facilities:display( "~nChecking final states: ok" ), %test_facilities:display( "Reference: ~s", % [ hashtable:to_string( First ) ] ), [ FirstList= OtherListedTable || { _, OtherListedTable } <- Others ].