@ -1,127 +1,83 @@
local inspect = require ' inspect '
local unindent = require ' spec.unindent '
local is_luajit , ffi = pcall ( require , ' ffi ' )
local has_rawlen = type ( _G.rawlen ) == ' function '
describe ( ' inspect ' , function ( )
describe ( ' numbers ' , function ( )
it ( ' works ' , function ( )
assert.equals ( " 1 " , inspect ( 1 ) )
assert.equals ( " 1.5 " , inspect ( 1.5 ) )
assert.equals ( " -3.14 " , inspect ( - 3.14 ) )
assert.equals ( inspect ( 1 ) , " 1 " )
assert.equals ( inspect ( 1.5 ) , " 1.5 " )
assert.equals ( inspect ( - 3.14 ) , " -3.14 " )
end )
end )
describe ( ' strings ' , function ( )
it ( ' puts quotes around regular strings ' , function ( )
assert.equals ( ' "hello" ' , inspect ( " hello " ) )
assert.equals ( inspect ( " hello " ) , ' "hello" ' )
end )
it ( ' puts apostrophes around strings with quotes ' , function ( )
assert.equals ( " 'I have \" quotes \" ' " , inspect ( ' I have "quotes" ' ) )
assert.equals ( inspect ( ' I have "quotes" ' ) , " 'I have \" quotes \" ' " )
end )
it ( ' uses regular quotes if the string has both quotes and apostrophes ' , function ( )
assert.equals ( ' "I have \\ "quotes \\ " and \' apostrophes \' " ' , inspect ( " I have \" quotes \" and 'apostrophes' " ) )
assert.equals ( inspect ( " I have \" quotes \" and 'apostrophes' " ) , ' "I have \\ "quotes \\ " and \' apostrophes \' " ' )
end )
it ( ' escapes newlines properly ' , function ( )
assert.equals ( ' "I have \\ n new \\ n lines" ' , inspect ( ' I have \n new \n lines ' ) )
assert.equals ( inspect ( ' I have \n new \n lines ' ) , ' "I have \\ n new \\ n lines" ' )
end )
it ( ' escapes tabs properly ' , function ( )
assert.equals ( ' "I have \\ t a tab character" ' , inspect ( ' I have \t a tab character ' ) )
assert.equals ( inspect ( ' I have \t a tab character ' ) , ' "I have \\ t a tab character" ' )
end )
it ( ' escapes backspaces properly ' , function ( )
assert.equals ( ' "I have \\ b a back space" ' , inspect ( ' I have \b a back space ' ) )
end )
it ( ' escapes unnamed control characters with 1 or 2 digits ' , function ( )
assert.equals ( ' "Here are some control characters: \\ 0 \\ 1 \\ 6 \\ 17 \\ 27 \\ 31" ' ,
inspect ( ' Here are some control characters: \0 \1 \6 \17 \27 \31 ' ) )
end )
it ( ' escapes unnamed control characters with 3 digits when they are followed by numbers ' , function ( )
assert.equals ( ' "Control chars followed by digits \\ 0001 \\ 0011 \\ 0061 \\ 0171 \\ 0271 \\ 0311" ' ,
inspect ( ' Control chars followed by digits \000 1 \001 1 \006 1 \017 1 \027 1 \031 1 ' ) )
assert.equals ( inspect ( ' I have \b a back space ' ) , ' "I have \\ b a back space" ' )
end )
it ( ' backslashes its backslashes ' , function ( )
assert.equals ( ' "I have \\ \\ a backslash" ' , inspect ( ' I have \\ a backslash ' ) )
assert.equals ( ' "I have \\ \\ \\ \\ two backslashes" ' , inspect ( ' I have \\ \\ two backslashes ' ) )
assert.equals ( ' "I have \\ \\ \\ n a backslash followed by a newline" ' ,
inspect ( ' I have \\ \n a backslash followed by a newline ' ) )
assert.equals ( inspect ( ' I have \\ a backslash ' ) , ' "I have \\ \\ a backslash" ' )
assert.equals ( inspect ( ' I have \\ \\ two backslashes ' ) , ' "I have \\ \\ \\ \\ two backslashes" ' )
assert.equals ( inspect ( ' I have \\ \n a backslash followed by a newline ' ) , ' "I have \\ \\ \\ n a backslash followed by a newline" ' )
end )
end )
it ( ' works with nil ' , function ( )
assert.equals ( ' nil ' , inspect ( nil ) )
assert.equals ( inspect ( nil ) , ' nil ' )
end )
it ( ' works with functions ' , function ( )
assert.equals ( ' { <function 1>, <function 2>, <function 1> } ' , inspect ( { print , type , print } ) )
assert.equals ( inspect ( { print , type , print } ) , ' { <function 1>, <function 2>, <function 1> } ' )
end )
it ( ' works with booleans ' , function ( )
assert.equals ( ' true ' , inspect ( true ) )
assert.equals ( ' false ' , inspect ( false ) )
assert.equals ( inspect ( true ) , ' true ' )
assert.equals ( inspect ( false ) , ' false ' )
end )
if is_luajit then
it ( ' works with luajit cdata ' , function ( )
assert.equals ( ' { cdata<int>: PTR, ctype<int>, cdata<int>: PTR } ' ,
inspect ( { ffi.new ( " int " , 1 ) , ffi.typeof ( " int " ) , ffi.typeof ( " int " ) ( 1 ) } ) : gsub ( ' (0x%x+) ' , ' PTR ' ) )
describe ( ' luajit cdata ' , function ( )
it ( ' works with luajit cdata ' , function ( )
assert.equals ( inspect ( { ffi.new ( " int " , 1 ) , ffi.typeof ( " int " ) , ffi.typeof ( " int " ) ( 1 ) } ) , ' { <cdata 1>, <cdata 2>, <cdata 3> } ' )
end )
end )
end
describe ( ' tables ' , function ( )
it ( ' works with simple array-like tables ' , function ( )
assert.equals ( " { 1, 2, 3 } " , inspect ( { 1 , 2 , 3 } ) )
assert.equals ( inspect ( { 1 , 2 , 3 } ) , " { 1, 2, 3 } " )
end )
it ( ' works with nested arrays ' , function ( )
assert.equals ( ' { "a", "b", "c", { "d", "e" }, "f" } ' , inspect ( { ' a ' , ' b ' , ' c ' , { ' d ' , ' e ' } , ' f ' } ) )
assert.equals ( inspect ( { ' a ' , ' b ' , ' c ' , { ' d ' , ' e ' } , ' f ' } ) , ' { "a", "b", "c", { "d", "e" }, "f" } ' )
end )
if has_rawlen then
it ( ' handles arrays with a __len metatable correctly (ignoring the __len metatable and using rawlen) ' , function ( )
local arr = setmetatable ( { 1 , 2 , 3 } , { __len = function ( ) return nil end } )
assert.equals ( " { 1, 2, 3, \n <metatable> = { \n __len = <function 1> \n } \n } " , inspect ( arr ) )
end )
it ( ' handles tables with a __pairs metamethod (ignoring the __pairs metamethod and using next) ' , function ( )
local t = setmetatable ( { { } , name = " yeah " } , { __pairs = function ( ) end } )
assert.equals (
unindent ( [ [ { { } ,
name = " yeah " ,
< metatable > = {
__pairs = < function 1 >
}
} ] ] ) ,
inspect ( t ) )
end )
end
it ( ' works with simple dictionary tables ' , function ( )
assert.equals ( " { \n a = 1, \n b = 2 \n } " , inspect ( { a = 1 , b = 2 } ) )
end )
it ( ' identifies tables with no number 1 as struct-like ' , function ( )
assert.equals ( unindent ( [ [ {
[ 2 ] = 1 ,
[ 25 ] = 1 ,
id = 1
}
] ] ) , inspect ( { [ 2 ] = 1 , [ 25 ] = 1 , id = 1 } ) )
end )
it ( ' identifies numeric non-array keys as dictionary keys ' , function ( )
assert.equals ( " { 1, 2, \n [-1] = true \n } " , inspect ( { 1 , 2 , [ - 1 ] = true } ) )
assert.equals ( " { 1, 2, \n [1.5] = true \n } " , inspect ( { 1 , 2 , [ 1.5 ] = true } ) )
assert.equals ( inspect ( { a = 1 , b = 2 } ) , " { \n a = 1, \n b = 2 \n } " )
end )
it ( ' sorts keys in dictionary tables ' , function ( )
@ -130,7 +86,7 @@ describe( 'inspect', function()
[ coroutine.create ( function ( ) end ) ] = 1 ,
[ 14 ] = 1 , [ { c = 2 } ] = 1 , [ true ] = 1
}
assert.equals ( unindent ( [ [
assert.equals ( inspect ( t ) , unindent ( [ [
{ 1 , 2 , 3 ,
[ 14 ] = 1 ,
[ true ] = 1 ,
@ -142,30 +98,30 @@ describe( 'inspect', function()
[ < function 1 > ] = 1 ,
[ < thread 1 > ] = 1
}
] ] ) , inspect ( t ) )
] ] ) )
end )
it ( ' works with nested dictionary tables ' , function ( )
assert.equals ( unindent ( [ [ {
assert.equals ( inspect ( { d = 3 , b = { c = 2 } , a = 1 } ) , unindent ( [ [ {
a = 1 ,
b = {
c = 2
} ,
d = 3
} ] ] ) , inspect ( { d = 3 , b = { c = 2 } , a = 1 } ) )
} ] ] ) )
end )
it ( ' works with hybrid tables ' , function ( )
assert.equals ( unindent ( [ [
assert.equals (
inspect ( { ' a ' , { b = 1 } , 2 , c = 3 , [ ' ahoy you ' ] = 4 } ) ,
unindent ( [ [
{ " a " , {
b = 1
} , 2 ,
[ " ahoy you " ] = 4 ,
c = 3
}
] ] ) , inspect ( { ' a ' , { b = 1 } , 2 , c = 3 , [ ' ahoy you ' ] = 4 } ) )
] ] ) )
end )
it ( ' displays <table x> instead of repeating an already existing table ' , function ( )
@ -174,7 +130,7 @@ describe( 'inspect', function()
a [ 4 ] = b
a [ 5 ] = a
a [ 6 ] = b
assert.equals ( '<1>{ 1, 2, 3, <2>{ "a", "b", "c", <table 1> }, <table 1>, <table 2> } ' , inspect ( a ) )
assert.equals ( inspect ( a ) , '<1>{ 1, 2, 3, <2>{ "a", "b", "c", <table 1> }, <table 1>, <table 2> } ' )
end )
describe ( ' The depth parameter ' , function ( )
@ -182,7 +138,7 @@ describe( 'inspect', function()
local keys = { [ level5 ] = true }
it ( ' has infinite depth by default ' , function ( )
assert.equals ( unindent ( [ [
assert.equals ( inspect ( level5 ) , unindent ( [ [
{ 1 , 2 , 3 ,
a = {
b = {
@ -194,24 +150,24 @@ describe( 'inspect', function()
}
}
}
] ] ) , inspect ( level5 ) )
] ] ) )
end )
it ( ' is modifiable by the user ' , function ( )
assert.equals ( unindent ( [ [
assert.equals ( inspect ( level5 , { depth = 2 } ) , unindent ( [ [
{ 1 , 2 , 3 ,
a = {
b = { ... }
}
}
] ] ) , inspect ( level5 , { depth = 2 } ) )
] ] ) )
assert.equals ( unindent ( [ [
assert.equals ( inspect ( level5 , { depth = 1 } ) , unindent ( [ [
{ 1 , 2 , 3 ,
a = { ... }
}
] ] ) , inspect ( level5 , { depth = 1 } ) )
] ] ) )
assert.equals ( unindent ( [ [
assert.equals ( inspect ( level5 , { depth = 4 } ) , unindent ( [ [
{ 1 , 2 , 3 ,
a = {
b = {
@ -221,13 +177,13 @@ describe( 'inspect', function()
}
}
}
] ] ) , inspect ( level5 , { depth = 4 } ) )
] ] ) )
assert.equals ( " {...} " , inspect ( level5 , { depth = 0 } ) )
assert.equals ( inspect ( level5 , { depth = 0 } ) , " {...} " )
end )
it ( ' respects depth on keys ' , function ( )
assert.equals ( unindent ( [ [
assert.equals ( inspect ( keys , { depth = 4 } ) , unindent ( [ [
{
[ { 1 , 2 , 3 ,
a = {
@ -237,7 +193,7 @@ describe( 'inspect', function()
}
} ] = true
}
] ] ) , inspect ( keys , { depth = 4 } ) )
] ] ) )
end )
end )
@ -245,7 +201,7 @@ describe( 'inspect', function()
it ( ' changes the substring used for newlines ' , function ( )
local t = { a = { b = 1 } }
assert.equal ( " {@ a = {@ b = 1@ }@} " , inspect ( t , { newline = ' @ ' } ) )
assert.equal ( inspect ( t , { newline = ' @ ' } ) , " {@ a = {@ b = 1@ }@} " )
end )
end )
@ -253,7 +209,7 @@ describe( 'inspect', function()
it ( ' changes the substring used for indenting ' , function ( )
local t = { a = { b = 1 } }
assert.equal ( " { \n >>>a = { \n >>>>>>b = 1 \n >>>} \n } " , inspect ( t , { indent = ' >>> ' } ) )
assert.equal ( inspect ( t , { indent = ' >>> ' } ) , " { \n >>>a = { \n >>>>>>b = 1 \n >>>} \n } " )
end )
end )
@ -262,56 +218,56 @@ describe( 'inspect', function()
it ( ' removes one element ' , function ( )
local names = { ' Andrew ' , ' Peter ' , ' Ann ' }
local removeAnn = function ( item ) if item ~= ' Ann ' then return item end end
assert.equals ( ' { "Andrew", "Peter" } ' , inspect ( names , { process = removeAnn } ) )
assert.equals ( inspect ( names , { process = removeAnn } ) , ' { "Andrew", "Peter" } ' )
end )
it ( ' uses the path ' , function ( )
local names = { ' Andrew ' , ' Peter ' , ' Ann ' }
local removeThird = function ( item , path ) if path [ 1 ] ~= 3 then return item end end
assert.equals ( ' { "Andrew", "Peter" } ' , inspect ( names , { process = removeThird } ) )
assert.equals ( inspect ( names , { process = removeThird } ) , ' { "Andrew", "Peter" } ' )
end )
it ( ' replaces items ' , function ( )
local names = { ' Andrew ' , ' Peter ' , ' Ann ' }
local filterAnn = function ( item ) return item == ' Ann ' and ' <filtered> ' or item end
assert.equals ( ' { "Andrew", "Peter", "<filtered>" } ' , inspect ( names , { process = filterAnn } ) )
assert.equals ( inspect ( names , { process = filterAnn } ) , ' { "Andrew", "Peter", "<filtered>" } ' )
end )
it ( ' nullifies metatables ' , function ( )
local mt = { ' world ' }
local t = setmetatable ( { ' hello ' } , mt )
local removeMt = function ( item ) if item ~= mt then return item end end
assert.equals ( ' { "hello" } ' , inspect ( t , { process = removeMt } ) )
assert.equals ( inspect ( t , { process = removeMt } ) , ' { "hello" } ' )
end )
it ( ' nullifies metatables using their paths ' , function ( )
local mt = { ' world ' }
local t = setmetatable ( { ' hello ' } , mt )
local removeMt = function ( item , path ) if path [ # path ] ~= inspect.METATABLE then return item end end
assert.equals ( ' { "hello" } ' , inspect ( t , { process = removeMt } ) )
assert.equals ( inspect ( t , { process = removeMt } ) , ' { "hello" } ' )
end )
it ( ' nullifies the root object ' , function ( )
local names = { ' Andrew ' , ' Peter ' , ' Ann ' }
local removeNames = function ( item ) if item ~= names then return item end end
assert.equals ( ' nil ' , inspect ( names , { process = removeNames } ) )
assert.equals ( inspect ( names , { process = removeNames } ) , ' nil ' )
end )
it ( ' changes keys ' , function ( )
local dict = { a = 1 }
local changeKey = function ( item ) return item == ' a ' and ' x ' or item end
assert.equals ( ' { \n x = 1 \n } ' , inspect ( dict , { process = changeKey } ) )
local changeKey = function ( item , path ) return item == ' a ' and ' x ' or item end
assert.equals ( inspect ( dict , { process = changeKey } ) , ' { \n x = 1 \n } ' )
end )
it ( ' nullifies keys ' , function ( )
local dict = { a = 1 , b = 2 }
local removeA = function ( item ) return item ~= ' a ' and item or nil end
assert.equals ( ' { \n b = 2 \n } ' , inspect ( dict , { process = removeA } ) )
local removeA = function ( item , path ) return item ~= ' a ' and item or nil end
assert.equals ( inspect ( dict , { process = removeA } ) , ' { \n b = 2 \n } ' )
end )
it ( ' prints inspect.KEY & inspect.METATABLE ' , function ( )
local t = { inspect.KEY , inspect.METATABLE }
assert.equals ( " { inspect.KEY, inspect.METATABLE } " , inspect ( t ) )
assert.equals ( inspect ( t ) , " { inspect.KEY, inspect.METATABLE } " )
end )
it ( ' marks key paths with inspect.KEY and metatables with inspect.METATABLE ' , function ( )
@ -325,7 +281,7 @@ describe( 'inspect', function()
inspect ( t , { process = addItem } )
assert.same ( {
assert.same ( items , {
{ item = t , path = { } } ,
{ item = { a = 1 } , path = { { a = 1 } , inspect.KEY } } ,
{ item = ' a ' , path = { { a = 1 } , inspect.KEY , ' a ' , inspect.KEY } } ,
@ -336,15 +292,9 @@ describe( 'inspect', function()
{ item = { c = 3 } , path = { { a = 1 } , inspect.METATABLE } } ,
{ item = ' c ' , path = { { a = 1 } , inspect.METATABLE , ' c ' , inspect.KEY } } ,
{ item = 3 , path = { { a = 1 } , inspect.METATABLE , ' c ' } }
} , items )
} )
end )
it ( ' handles recursive tables correctly ' , function ( )
local tbl = { 1 , 2 , 3 }
tbl.loop = tbl
inspect ( tbl , { process = function ( x ) return x end } )
end )
end )
describe ( ' metatables ' , function ( )
@ -352,7 +302,7 @@ describe( 'inspect', function()
it ( ' includes the metatable as an extra hash attribute ' , function ( )
local foo = { foo = 1 , __mode = ' v ' }
local bar = setmetatable ( { a = 1 } , foo )
assert.equals ( unindent ( [ [
assert.equals ( inspect ( bar ) , unindent ( [ [
{
a = 1 ,
< metatable > = {
@ -360,101 +310,60 @@ describe( 'inspect', function()
foo = 1
}
}
] ] ) , inspect ( bar ) )
] ] ) )
end )
it ( ' can be used on the __tostring metamethod of a table without error s' , function ( )
local f = function ( x ) return inspect ( x ) end
local tbl = setmetatable ( { x = 1 } , { __tostring = f } )
assert.equals ( unindent ( [ [
{
x = 1 ,
it ( ' includes the __tostring metamethod if it exist s' , function ( )
local foo = { foo = 1 , __tostring = function ( ) return ' hello \n world ' end }
local bar = setmetatable ( { a = 1 } , foo )
assert.equals ( inspect ( bar ) , unindent ( [ [
{ -- hello\nworld
a = 1 ,
< metatable > = {
__tostring = < function 1 >
__tostring = < function 1 > ,
foo = 1
}
}
] ] ) , tostring ( tbl ) )
end )
it ( ' does not allow collecting weak tables while they are being inspected ' , function ( )
collectgarbage ( ' stop ' )
finally ( function ( ) collectgarbage ( ' restart ' ) end )
local shimMetatable = {
__mode = ' v ' ,
__index = function ( ) return { } end ,
}
local function shim ( ) return setmetatable ( { } , shimMetatable ) end
local t = shim ( )
t.key = shim ( )
assert.equals ( unindent ( [ [
{
key = {
< metatable > = < 1 > {
__index = < function 1 > ,
__mode = " v "
}
} ,
< metatable > = < table 1 >
}
] ] ) , inspect ( t ) )
] ] ) )
end )
it ( ' ignores metatables with __metatable field set to non-nil and non-table type ' , function ( )
local function process ( item ) return item end
local function inspector ( data ) return inspect ( data , { process = process } ) end
local foo = setmetatable ( { } , { __metatable = false } )
local bar = setmetatable ( { } , { __metatable = true } )
local baz = setmetatable ( { } , { __metatable = 10 } )
local spam = setmetatable ( { } , { __metatable = nil } )
local eggs = setmetatable ( { } , { __metatable = { } } )
assert.equals ( unindent ( ' {} ' ) , inspector ( foo ) )
assert.equals ( unindent ( ' {} ' ) , inspector ( bar ) )
assert.equals ( unindent ( ' {} ' ) , inspector ( baz ) )
assert.equals ( unindent ( [ [
{
< metatable > = { }
}
] ] ) , inspector ( spam ) )
assert.equals ( unindent ( [ [
{
< metatable > = { }
it ( ' includes an error string if __tostring metamethod throws an error ' , function ( )
local foo = { foo = 1 , __tostring = function ( ) error ( ' hello ' , 0 ) end }
local bar = setmetatable ( { a = 1 } , foo )
assert.equals ( inspect ( bar ) , unindent ( [ [
{ -- error: hello
a = 1 ,
< metatable > = {
__tostring = < function 1 > ,
foo = 1
}
}
] ] ) , inspector ( eggs ) )
] ] ) )
end )
describe ( ' When a table is its own metatable ' , function ( )
it ( ' accepts a table that is its own metatable without stack overflowing ' , function ( )
local x = { }
setmetatable ( x , x )
assert.equals ( unindent ( [ [
assert.equals ( inspect ( x ) , unindent ( [ [
< 1 > {
< metatable > = < table 1 >
}
] ] ) , inspect ( x ) )
] ] ) )
end )
it ( ' can invoke the __tostring method without stack overflowing ' , function ( )
local t = { }
t.__index = t
setmetatable ( t , t )
assert.equals ( unindent ( [ [
assert.equals ( inspect ( t ) , unindent ( [ [
< 1 > {
__index = < table 1 > ,
< metatable > = < table 1 >
}
] ] ) , inspect ( t ) )
] ] ) )
end )
end )
end )
end )
it ( ' allows changing the global tostring ' , function ( )
local save = _G.tostring
_G.tostring = inspect
local s = tostring ( { 1 , 2 , 3 } )
_G.tostring = save
assert.equals ( " { 1, 2, 3 } " , s )
end )
end )