Add missing comments
This commit is contained in:
parent
941f0a4886
commit
037f8d80d2
@ -1,3 +1,7 @@
|
|||||||
|
# Most of the external functionality is in the writer
|
||||||
|
|
||||||
|
# if you want some attributes not written also check volotile
|
||||||
|
|
||||||
require_relative "sof/util"
|
require_relative "sof/util"
|
||||||
require_relative "sof/node"
|
require_relative "sof/node"
|
||||||
require_relative "sof/simple_node"
|
require_relative "sof/simple_node"
|
||||||
|
@ -1,13 +1,39 @@
|
|||||||
|
# If a class defines to_sof_node it tells the write that it will generate Nodes itself
|
||||||
|
# this delegates to array_to_sof_node
|
||||||
|
Array.class_eval do
|
||||||
|
def to_sof_node(writer , level , ref )
|
||||||
|
Sof.array_to_sof_node(self , writer , level , ref )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
module Sof
|
module Sof
|
||||||
|
# Creates a ArrayNode (see there) for the Array.
|
||||||
|
# This mainly involves creating nodes for the children
|
||||||
|
def self.array_to_sof_node(array , writer , level , ref )
|
||||||
|
node = Sof::ArrayNode.new(ref)
|
||||||
|
array.each do |object|
|
||||||
|
node.add writer.to_sof_node( object , level + 1)
|
||||||
|
end
|
||||||
|
node
|
||||||
|
end
|
||||||
|
|
||||||
|
# A ArrayNode is a Node for a Array. See Node for when and how nodes are used in Sof.
|
||||||
|
# A ArrayNode has a list of children that hold the value node representations for
|
||||||
|
# the arrays values.
|
||||||
|
#
|
||||||
class ArrayNode < Node
|
class ArrayNode < Node
|
||||||
def initialize ref
|
def initialize ref
|
||||||
super(ref)
|
super(ref)
|
||||||
@children = []
|
@children = []
|
||||||
end
|
end
|
||||||
attr_reader :children
|
attr_reader :children
|
||||||
|
|
||||||
def add c
|
def add c
|
||||||
@children << c
|
@children << c
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The output of a Array can be a long or a short format
|
||||||
|
# The short is used for 7 or less SimpleNodes
|
||||||
def out io , level = 0
|
def out io , level = 0
|
||||||
super
|
super
|
||||||
short = true
|
short = true
|
||||||
@ -22,6 +48,8 @@ module Sof
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
# This defines the short output which is basically what you would write in ruby
|
||||||
|
# ie [ value1 , value2 , ...]
|
||||||
def short_out(io,level)
|
def short_out(io,level)
|
||||||
io.write("[")
|
io.write("[")
|
||||||
@children.each_with_index do |child , i|
|
@children.each_with_index do |child , i|
|
||||||
@ -30,6 +58,9 @@ module Sof
|
|||||||
end
|
end
|
||||||
io.write("]")
|
io.write("]")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Arrays start with the minus on each line "-"
|
||||||
|
# and each line has the value
|
||||||
def long_out io , level
|
def long_out io , level
|
||||||
indent = " " * level
|
indent = " " * level
|
||||||
@children.each_with_index do |child , i|
|
@children.each_with_index do |child , i|
|
||||||
@ -39,18 +70,4 @@ module Sof
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.array_to_sof_node(array , writer , level , ref )
|
|
||||||
node = Sof::ArrayNode.new(ref)
|
|
||||||
array.each do |object|
|
|
||||||
node.add writer.to_sof_node( object , level + 1)
|
|
||||||
end
|
|
||||||
node
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
Array.class_eval do
|
|
||||||
def to_sof_node(writer , level , ref )
|
|
||||||
Sof.array_to_sof_node(self , writer , level , ref )
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,13 +1,41 @@
|
|||||||
|
Hash.class_eval do
|
||||||
|
# If a class defines to_sof_node it tells the write that it will generate Nodes itself
|
||||||
|
# this delegates to hash_to_sof_node
|
||||||
|
def to_sof_node(writer , level , ref)
|
||||||
|
Sof.hash_to_sof_node( self , writer , level , ref)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
module Sof
|
module Sof
|
||||||
|
# Creates a HashNode (see there) for the Hash.
|
||||||
|
# This mainly involves creating nodes for key value pairs
|
||||||
|
def self.hash_to_sof_node(hash,writer , level , ref)
|
||||||
|
node = Sof::HashNode.new(ref)
|
||||||
|
hash.each do |key , object|
|
||||||
|
k = writer.to_sof_node( key ,level + 1)
|
||||||
|
v = writer.to_sof_node( object ,level + 1)
|
||||||
|
node.add(k , v)
|
||||||
|
end
|
||||||
|
node
|
||||||
|
end
|
||||||
|
|
||||||
|
# A HashNode is a Node for a Hash. See Node for when and how nodes are used in Sof.
|
||||||
|
# A HashNode has a list of children that hold the key/value node representations for
|
||||||
|
# the hashes keys and values.
|
||||||
|
|
||||||
class HashNode < Node
|
class HashNode < Node
|
||||||
def initialize ref
|
def initialize ref
|
||||||
super(ref)
|
super(ref)
|
||||||
@children = []
|
@children = []
|
||||||
end
|
end
|
||||||
attr_reader :children
|
attr_reader :children
|
||||||
|
|
||||||
def add key , val
|
def add key , val
|
||||||
@children << [key,val]
|
@children << [key,val]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The output of a Hash can be a long or a short format
|
||||||
|
# The short is used for 7 or less SimpleNodes
|
||||||
def out io , level = 0
|
def out io , level = 0
|
||||||
super
|
super
|
||||||
short = true
|
short = true
|
||||||
@ -21,6 +49,10 @@ module Sof
|
|||||||
long_out(io , level)
|
long_out(io , level)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# This defines the short output which is basically what you would write in ruby
|
||||||
|
# ie { key1 => value1 , ... }
|
||||||
def short_out(io,level)
|
def short_out(io,level)
|
||||||
io.write("{")
|
io.write("{")
|
||||||
children.each_with_index do |child , i|
|
children.each_with_index do |child , i|
|
||||||
@ -32,6 +64,10 @@ module Sof
|
|||||||
end
|
end
|
||||||
io.write("}")
|
io.write("}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The long output is like an array of associations.
|
||||||
|
# Arrays start with the minus on each line "-"
|
||||||
|
# and each line has the association key => value, same as used for the {} syntax
|
||||||
def long_out io , level
|
def long_out io , level
|
||||||
indent = " " * level
|
indent = " " * level
|
||||||
children.each_with_index do |child , i|
|
children.each_with_index do |child , i|
|
||||||
@ -44,20 +80,4 @@ module Sof
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.hash_to_sof_node(hash,writer , level , ref)
|
|
||||||
node = Sof::HashNode.new(ref)
|
|
||||||
hash.each do |key , object|
|
|
||||||
k = writer.to_sof_node( key ,level + 1)
|
|
||||||
v = writer.to_sof_node( object ,level + 1)
|
|
||||||
node.add(k , v)
|
|
||||||
end
|
|
||||||
node
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Hash.class_eval do
|
|
||||||
def to_sof_node(writer , level , ref)
|
|
||||||
Sof.hash_to_sof_node( self , writer , level , ref)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,24 +1,40 @@
|
|||||||
module Sof
|
module Sof
|
||||||
|
|
||||||
|
# Members are members of the graph to be written
|
||||||
|
# The class collects all reachable objects into a hash for further transformation
|
||||||
|
|
||||||
class Members
|
class Members
|
||||||
include Util
|
include Util
|
||||||
|
|
||||||
|
# initialize with the "root" object
|
||||||
|
# any object really that then becomes the root.
|
||||||
|
# the result is easier to read if it really is a root
|
||||||
|
# All reachable objects are collected into "objects" hash
|
||||||
|
# The class keeps a counter for references encountered and creates unique
|
||||||
|
# occurences for each object based on object_id (not == or ===)
|
||||||
def initialize root
|
def initialize root
|
||||||
@root = root
|
@root = root
|
||||||
@counter = 1
|
@counter = 1
|
||||||
@objects = {}
|
@objects = {}
|
||||||
@referenced = false
|
|
||||||
add(root , 0)
|
add(root , 0)
|
||||||
end
|
end
|
||||||
attr_reader :objects , :root , :referenced
|
attr_reader :objects , :root
|
||||||
|
|
||||||
|
private
|
||||||
|
# recursively add reachable objects from this object
|
||||||
|
# this is called from the initialize and is private
|
||||||
def add object , level
|
def add object , level
|
||||||
|
# not storing simple (value) objects
|
||||||
return if is_value?(object)
|
return if is_value?(object)
|
||||||
|
|
||||||
|
# see if we we came accross this before
|
||||||
if( occurence = @objects[object.object_id] )
|
if( occurence = @objects[object.object_id] )
|
||||||
#puts "reset level #{level} at #{occurence.level}"
|
#puts "reset level #{level} at #{occurence.level}"
|
||||||
if occurence.level > level
|
if occurence.level > level
|
||||||
occurence.level = level
|
#always store the most shallow level
|
||||||
|
occurence.level = level
|
||||||
end
|
end
|
||||||
|
# and only one Occurence for each object, create a reference for the second occurence
|
||||||
unless occurence.referenced
|
unless occurence.referenced
|
||||||
#puts "referencing #{@counter} , at level #{level}/#{occurence.level} "
|
#puts "referencing #{@counter} , at level #{level}/#{occurence.level} "
|
||||||
occurence.set_reference(@counter)
|
occurence.set_reference(@counter)
|
||||||
@ -26,18 +42,24 @@ module Sof
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# if first time see, create and store Occurence
|
||||||
o = Occurence.new( object , level )
|
o = Occurence.new( object , level )
|
||||||
@objects[object.object_id] = o
|
@objects[object.object_id] = o
|
||||||
|
|
||||||
|
# and recursively add attributes
|
||||||
attributes = attributes_for(object)
|
attributes = attributes_for(object)
|
||||||
attributes.each do |a|
|
attributes.each do |a|
|
||||||
val = get_value( object , a)
|
val = get_value( object , a)
|
||||||
add(val , level + 1)
|
add(val , level + 1)
|
||||||
end
|
end
|
||||||
|
# and array values
|
||||||
if( object.is_a? Array )
|
if( object.is_a? Array )
|
||||||
object.each do |a|
|
object.each do |a|
|
||||||
add(a , level + 1)
|
add(a , level + 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# and hash keys/values
|
||||||
if( object.is_a? Hash )
|
if( object.is_a? Hash )
|
||||||
object.each do |a,b|
|
object.each do |a,b|
|
||||||
add(a , level + 1)
|
add(a , level + 1)
|
||||||
@ -46,4 +68,6 @@ module Sof
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# TODO, since this class only has one function, and one instance
|
||||||
|
# it could be merged as class functions to Occurence
|
||||||
end
|
end
|
||||||
|
@ -1,22 +1,37 @@
|
|||||||
# We transform objects into a tree of nodes
|
# We transform objects into a tree of nodes
|
||||||
|
|
||||||
module Sof
|
module Sof
|
||||||
# abstract base class for nodes in the tree
|
|
||||||
# may be referenced (should be a simple name or number)
|
# Before writing the objects are transformed into a tree of nodes.
|
||||||
|
# as the Members (all objects) are a graph (not tree) this is achieved by referencing
|
||||||
|
#
|
||||||
|
# There are only two subclasses, SimpleNode and ObejctNode, for simple or not
|
||||||
|
# The base class only holds the referenced flag
|
||||||
|
# Also nodes must implement the out function
|
||||||
|
|
||||||
class Node
|
class Node
|
||||||
include Util
|
include Util
|
||||||
|
# only has one attribute, the referenced flag
|
||||||
|
# This could be anything, but we use a simple number counter which is handled in the Members
|
||||||
|
# class during construction of the occurrence hash
|
||||||
def initialize ref
|
def initialize ref
|
||||||
|
#puts "node has ref #{self.class}:#{ref}" if ref
|
||||||
@referenced = ref
|
@referenced = ref
|
||||||
end
|
end
|
||||||
|
attr_reader :referenced
|
||||||
|
|
||||||
# must be able to output to a stream
|
# must be able to output to a stream
|
||||||
|
# This function must be called as super as it handles possible reference marker "& num"
|
||||||
def out io ,level
|
def out io ,level
|
||||||
io.write "&#{@referenced} " if @referenced
|
io.write "&#{@referenced} " if @referenced
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# helper function to return the output as a string
|
||||||
|
# ie creates stringio, calls out and returns the string
|
||||||
def as_string(level)
|
def as_string(level)
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
out(io,level)
|
out(io,level)
|
||||||
io.string
|
io.string
|
||||||
end
|
end
|
||||||
attr_reader :referenced
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,18 +2,27 @@
|
|||||||
module Sof
|
module Sof
|
||||||
|
|
||||||
# ObjectNode means node with structure
|
# ObjectNode means node with structure
|
||||||
# ie arrays and hashes get transformed into these too
|
# ie arrays and hashes get transformed into these too, as well as objects with attributes
|
||||||
|
|
||||||
class ObjectNode < Node
|
class ObjectNode < Node
|
||||||
|
|
||||||
|
# init with a string, much like a simple node
|
||||||
|
# structure is added after construction and kept in a children array
|
||||||
def initialize data , ref
|
def initialize data , ref
|
||||||
super(ref)
|
super(ref)
|
||||||
@data = data
|
@data = data
|
||||||
@children = []
|
@children = []
|
||||||
end
|
end
|
||||||
attr_reader :children , :data
|
attr_reader :children , :data
|
||||||
|
|
||||||
|
# children array hold key value pairs
|
||||||
def add k , v
|
def add k , v
|
||||||
@children << [k,v]
|
@children << [k,v]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# write out at the given level
|
||||||
|
# level determines the indentation (level * space)
|
||||||
|
# write out the data and then the children (always key value on one line)
|
||||||
def out io , level = 0
|
def out io , level = 0
|
||||||
super
|
super
|
||||||
io.write(@data)
|
io.write(@data)
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
module Sof
|
module Sof
|
||||||
|
|
||||||
|
# simple struct like class to wrap an object and hold additionally
|
||||||
|
# - the shallowest level at which it was seen
|
||||||
|
# - A possible reference
|
||||||
|
# - the fact if it has been written (for referenced objects)
|
||||||
class Occurence
|
class Occurence
|
||||||
def initialize object , level
|
def initialize object , level
|
||||||
@object = object
|
@object = object
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
# We transform objects into a tree of nodes
|
|
||||||
|
|
||||||
module Sof
|
module Sof
|
||||||
|
|
||||||
# What makes a node simple is that it has no further structure
|
# What makes a node simple is that it has no further structure
|
||||||
#
|
#
|
||||||
|
# This may mean number/string/symbol, but also tiny arrays or objects with
|
||||||
|
# very little attributes. In other words simple/object is not the same distinction
|
||||||
|
# as value/not value
|
||||||
|
|
||||||
class SimpleNode < Node
|
class SimpleNode < Node
|
||||||
|
|
||||||
|
# data is a string that is written out in "out" function
|
||||||
def initialize data , ref = nil
|
def initialize data , ref = nil
|
||||||
super(ref)
|
super(ref)
|
||||||
@data = data
|
@data = data
|
||||||
end
|
end
|
||||||
attr_reader :data
|
attr_reader :data
|
||||||
|
|
||||||
|
# just write the data given in construcor. simple. hence the name.
|
||||||
def out io , level
|
def out io , level
|
||||||
super(io,level)
|
super(io,level)
|
||||||
io.write(data)
|
io.write(data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
module Sof
|
module Sof
|
||||||
|
|
||||||
|
# module for a couple of helpers that are needed in Members and Writer
|
||||||
|
|
||||||
module Util
|
module Util
|
||||||
|
# "value" is a property meaning simple/ not further structure
|
||||||
|
# hence int/bool/string etc are values
|
||||||
def is_value? o
|
def is_value? o
|
||||||
return true if [true , false , nil].include?(o)
|
return true if [true , false , nil].include?(o)
|
||||||
return true if [Fixnum, Symbol, String].include?(o.class)
|
return true if [Fixnum, Symbol, String].include?(o.class)
|
||||||
@ -9,13 +14,22 @@ module Sof
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# extract an attribute by the given name from the object
|
||||||
|
# done with instance_variable_get
|
||||||
def get_value(object,name)
|
def get_value(object,name)
|
||||||
object.instance_variable_get "@#{name}".to_sym
|
object.instance_variable_get "@#{name}".to_sym
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# return a list of attributes for a given object
|
||||||
|
# attributes may be supressed with Volotile
|
||||||
|
# TODO should be able to specify order too
|
||||||
def attributes_for object
|
def attributes_for object
|
||||||
Sof::Util.attributes(object)
|
Sof::Util.attributes(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# return a list of attributes for a given object
|
||||||
|
# attributes may be supressed with Volotile
|
||||||
|
# TODO should be able to specify order too
|
||||||
def self.attributes( object )
|
def self.attributes( object )
|
||||||
atts = object.instance_variables.collect{|i| i.to_s[1..-1].to_sym } # chop of @
|
atts = object.instance_variables.collect{|i| i.to_s[1..-1].to_sym } # chop of @
|
||||||
atts - Volotile.attributes(object.class)
|
atts - Volotile.attributes(object.class)
|
||||||
|
@ -1,11 +1,27 @@
|
|||||||
module Sof
|
module Sof
|
||||||
class Volotile
|
|
||||||
|
# Volotile module keeps track of attributes that are not menat to be written
|
||||||
|
# The idea being similar to private methods. So not every little detail is relevant
|
||||||
|
# for the object. Some attribuets may be calculated, cached etc,
|
||||||
|
#
|
||||||
|
# There is only one useful call for the user, "add" attributes for a given class
|
||||||
|
#
|
||||||
|
module Volotile
|
||||||
@@mapping = { }
|
@@mapping = { }
|
||||||
def self.attributes clazz
|
|
||||||
@@mapping[clazz] || []
|
# Add attributes that are then ommited from the sof writing process
|
||||||
end
|
# The clazz is the real class object (eg String), and thus the
|
||||||
|
# adding must happen after the class definition, often at the end of the file
|
||||||
|
# attributes are an array of Symbols
|
||||||
def self.add clazz , attributes
|
def self.add clazz , attributes
|
||||||
@@mapping[clazz] = attributes
|
@@mapping[clazz] = attributes
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# return the volotile attributes as an array (or empty array)
|
||||||
|
def self.attributes clazz
|
||||||
|
@@mapping[clazz] || []
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,10 +1,29 @@
|
|||||||
module Sof
|
module Sof
|
||||||
|
|
||||||
|
# this function writes the object (and all reachable objects) out as sof
|
||||||
|
# and returns a string
|
||||||
|
# For trees or graphs this works best by handing roots
|
||||||
|
# Internally this is done in three steps:
|
||||||
|
# - All reachable objects are collected, these are called Occurences and the Members class does
|
||||||
|
# the collecting. Members holds a hash of occurences
|
||||||
|
# - A tree of nodes is created from the occurences. Different node classes for different classes
|
||||||
|
# - The nodes are witten to a steam
|
||||||
|
def self.write object
|
||||||
|
writer = Writer.new(Members.new(object) )
|
||||||
|
writer.write
|
||||||
|
end
|
||||||
|
|
||||||
|
# The writer does the coordinating work of the stages (see write function)
|
||||||
class Writer
|
class Writer
|
||||||
include Util
|
include Util
|
||||||
|
|
||||||
|
# Initialized with the Members (hash of occurences, see there)
|
||||||
def initialize members
|
def initialize members
|
||||||
@members = members
|
@members = members
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# main function, creates nodes from the occurences and writes the nodes to a string
|
||||||
|
# returns the sof formatted string for all objects
|
||||||
def write
|
def write
|
||||||
node = to_sof_node(@members.root , 0)
|
node = to_sof_node(@members.root , 0)
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
@ -12,6 +31,12 @@ module Sof
|
|||||||
io.string
|
io.string
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# create a Node (subclass) for an object at a given level.
|
||||||
|
# Level is mainly needed for the indenting
|
||||||
|
# from the object we get the Occurence and decide wether a reference node is needed
|
||||||
|
# simple objects (with more inner structure) become SimpleNodes
|
||||||
|
# Any structured object bocomes a ObjectNode
|
||||||
|
# Hash and Array create their own nodes via to_sof_node functions on the classes
|
||||||
def to_sof_node(object , level)
|
def to_sof_node(object , level)
|
||||||
if is_value?(object)
|
if is_value?(object)
|
||||||
return SimpleNode.new(object.to_sof())
|
return SimpleNode.new(object.to_sof())
|
||||||
@ -35,6 +60,10 @@ module Sof
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# create an object node from the object
|
||||||
|
# simple nodes are returned for small objects
|
||||||
|
# small means only simple attributes and only 30 chars of them
|
||||||
|
# object nodes are basically arrays (see there)
|
||||||
def object_sof_node( object , level , ref)
|
def object_sof_node( object , level , ref)
|
||||||
if( object.is_a? Class )
|
if( object.is_a? Class )
|
||||||
return SimpleNode.new( object.name , ref )
|
return SimpleNode.new( object.name , ref )
|
||||||
@ -55,11 +84,5 @@ module Sof
|
|||||||
end
|
end
|
||||||
node
|
node
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.write object
|
|
||||||
writer = Writer.new(Members.new(object) )
|
|
||||||
writer.write
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -17,7 +17,7 @@ require 'salama-object-file'
|
|||||||
|
|
||||||
module Checker
|
module Checker
|
||||||
def check should
|
def check should
|
||||||
out = Sof::Writer.write(@out)
|
out = Sof.write(@out)
|
||||||
same = (should == out)
|
same = (should == out)
|
||||||
puts "Shouldda\n#{out}" unless same
|
puts "Shouldda\n#{out}" unless same
|
||||||
assert_equal should , out
|
assert_equal should , out
|
||||||
|
Loading…
x
Reference in New Issue
Block a user