From 57387de9fa64eeb3a832d4c2380924de1c0d839e Mon Sep 17 00:00:00 2001 From: Vasundhara Jagdale Date: Mon, 30 Oct 2017 09:31:53 +0530 Subject: [PATCH] [MSYS-649] Fix InSpec file size in Windows, refactor File classes (#193) * Moved methods to respective classes for file and fixed specs Signed-off-by: Vasu1105 * Removed local_file.rb Signed-off-by: Vasu1105 * Fixed review comments Signed-off-by: Vasu1105 * Resolved conflicts after rebasing master and updated qnx file support changes as per new structure Signed-off-by: Vasu1105 * Fixed review comments Signed-off-by: Vasu1105 --- lib/train/extras.rb | 6 - lib/train/extras/file_aix.rb | 20 -- lib/train/extras/file_linux.rb | 16 -- lib/train/extras/file_qnx.rb | 34 --- lib/train/extras/file_unix.rb | 79 ------- lib/train/extras/file_windows.rb | 100 --------- lib/train/{extras/file_common.rb => file.rb} | 157 ++++++-------- lib/train/file/local.rb | 70 ++++++ lib/train/file/local/unix.rb | 77 +++++++ lib/train/file/local/windows.rb | 63 ++++++ lib/train/file/remote.rb | 28 +++ lib/train/file/remote/aix.rb | 21 ++ lib/train/file/remote/linux.rb | 19 ++ lib/train/file/remote/qnx.rb | 41 ++++ lib/train/file/remote/unix.rb | 110 ++++++++++ lib/train/file/remote/windows.rb | 94 ++++++++ lib/train/plugins/base_connection.rb | 1 + lib/train/transports/docker.rb | 9 +- lib/train/transports/local.rb | 8 +- lib/train/transports/local_file.rb | 98 --------- lib/train/transports/mock.rb | 6 +- lib/train/transports/ssh_connection.rb | 8 +- lib/train/transports/winrm_connection.rb | 2 +- .../tests/path_block_device_test.rb | 4 +- .../tests/path_character_device_test.rb | 4 +- test/integration/tests/path_file_test.rb | 4 +- test/integration/tests/path_folder_test.rb | 10 +- test/integration/tests/path_missing_test.rb | 1 - test/integration/tests/path_pipe_test.rb | 5 +- test/integration/tests/path_symlink_test.rb | 4 +- test/unit/extras/file_common_test.rb | 180 ---------------- test/unit/extras/os_detect_linux_test.rb | 6 +- test/unit/extras/os_detect_windows_test.rb | 2 +- test/unit/extras/windows_file_test.rb | 44 ---- test/unit/file/local/unix_test.rb | 112 ++++++++++ test/unit/file/local/windows_test.rb | 41 ++++ test/unit/file/local_test.rb | 110 ++++++++++ .../remote/linux_test.rb} | 14 +- test/unit/file/remote/unix_test.rb | 44 ++++ test/unit/file/remote_test.rb | 62 ++++++ test/unit/file_test.rb | 156 ++++++++++++++ test/unit/plugins/transport_test.rb | 2 +- test/unit/transports/local_file_test.rb | 202 ------------------ test/unit/transports/mock_test.rb | 6 +- test/windows/local_test.rb | 106 +++++++++ test/windows/winrm_test.rb | 125 +++++++++++ 46 files changed, 1397 insertions(+), 914 deletions(-) delete mode 100644 lib/train/extras/file_aix.rb delete mode 100644 lib/train/extras/file_linux.rb delete mode 100644 lib/train/extras/file_qnx.rb delete mode 100644 lib/train/extras/file_unix.rb delete mode 100644 lib/train/extras/file_windows.rb rename lib/train/{extras/file_common.rb => file.rb} (58%) create mode 100644 lib/train/file/local.rb create mode 100644 lib/train/file/local/unix.rb create mode 100644 lib/train/file/local/windows.rb create mode 100644 lib/train/file/remote.rb create mode 100644 lib/train/file/remote/aix.rb create mode 100644 lib/train/file/remote/linux.rb create mode 100644 lib/train/file/remote/qnx.rb create mode 100644 lib/train/file/remote/unix.rb create mode 100644 lib/train/file/remote/windows.rb delete mode 100644 lib/train/transports/local_file.rb delete mode 100644 test/unit/extras/file_common_test.rb delete mode 100644 test/unit/extras/windows_file_test.rb create mode 100644 test/unit/file/local/unix_test.rb create mode 100644 test/unit/file/local/windows_test.rb create mode 100644 test/unit/file/local_test.rb rename test/unit/{extras/linux_file_test.rb => file/remote/linux_test.rb} (95%) create mode 100644 test/unit/file/remote/unix_test.rb create mode 100644 test/unit/file/remote_test.rb create mode 100644 test/unit/file_test.rb delete mode 100644 test/unit/transports/local_file_test.rb diff --git a/lib/train/extras.rb b/lib/train/extras.rb index 6d450e21..a9ed8118 100644 --- a/lib/train/extras.rb +++ b/lib/train/extras.rb @@ -4,12 +4,6 @@ module Train::Extras require 'train/extras/command_wrapper' - require 'train/extras/file_common' - require 'train/extras/file_unix' - require 'train/extras/file_aix' - require 'train/extras/file_qnx' - require 'train/extras/file_linux' - require 'train/extras/file_windows' require 'train/extras/os_common' require 'train/extras/stat' diff --git a/lib/train/extras/file_aix.rb b/lib/train/extras/file_aix.rb deleted file mode 100644 index 9f3e7996..00000000 --- a/lib/train/extras/file_aix.rb +++ /dev/null @@ -1,20 +0,0 @@ -# encoding: utf-8 - -require 'shellwords' -require 'train/extras/stat' - -module Train::Extras - class AixFile < UnixFile - def link_path - return nil unless symlink? - @link_path ||= - @backend.run_command("perl -e 'print readlink shift' #{@spath}") - .stdout.chomp - end - - def mounted - @mounted ||= - @backend.run_command("lsfs -c #{@spath}") - end - end -end diff --git a/lib/train/extras/file_linux.rb b/lib/train/extras/file_linux.rb deleted file mode 100644 index 44f518c8..00000000 --- a/lib/train/extras/file_linux.rb +++ /dev/null @@ -1,16 +0,0 @@ -# encoding: utf-8 -# author: Dominik Richter -# author: Christoph Hartmann - -module Train::Extras - class LinuxFile < UnixFile - def content - return @content if defined?(@content) - @content = @backend.run_command( - "cat #{@spath} || echo -n").stdout - return @content unless @content.empty? - @content = nil if directory? or size.nil? or size > 0 - @content - end - end -end diff --git a/lib/train/extras/file_qnx.rb b/lib/train/extras/file_qnx.rb deleted file mode 100644 index 0b61ee53..00000000 --- a/lib/train/extras/file_qnx.rb +++ /dev/null @@ -1,34 +0,0 @@ -# encoding: utf-8 -# author: Christoph Hartmann -# author: Dominik Richter - -module Train::Extras - class QnxFile < UnixFile - def content - cat = 'cat' - cat = '/proc/boot/cat' if @backend.os[:release].to_i >= 7 - @content ||= case - when !exist? - nil - else - @backend.run_command("#{cat} #{@spath}").stdout || '' - end - end - - def type - if @backend.run_command("file #{@spath}").stdout.include?('directory') - return :directory - else - return :file - end - end - - %w{ - mode owner group uid gid mtime size selinux_label link_path mounted stat - }.each do |field| - define_method field.to_sym do - fail NotImplementedError, "QNX does not implement the #{m}() method yet." - end - end - end -end diff --git a/lib/train/extras/file_unix.rb b/lib/train/extras/file_unix.rb deleted file mode 100644 index 08b0f563..00000000 --- a/lib/train/extras/file_unix.rb +++ /dev/null @@ -1,79 +0,0 @@ -# encoding: utf-8 -# author: Dominik Richter -# author: Christoph Hartmann - -require 'shellwords' -require 'train/extras/stat' - -module Train::Extras - class UnixFile < FileCommon - attr_reader :path - def initialize(backend, path, follow_symlink = true) - super(backend, path, follow_symlink) - @spath = Shellwords.escape(@path) - end - - def content - @content ||= case - when !exist?, directory? - nil - when size.nil?, size == 0 - '' - else - @backend.run_command("cat #{@spath}").stdout || '' - end - end - - def exist? - @exist ||= ( - f = @follow_symlink ? '' : " || test -L #{@spath}" - @backend.run_command("test -e #{@spath}"+f) - .exit_status == 0 - ) - end - - def path - return @path unless @follow_symlink && symlink? - @link_path ||= read_target_path - end - - def mounted - @mounted ||= - @backend.run_command("mount | grep -- ' on #{@spath} '") - end - - %w{ - type mode owner group uid gid mtime size selinux_label - }.each do |field| - define_method field.to_sym do - stat[field.to_sym] - end - end - - def product_version - nil - end - - def file_version - nil - end - - def stat - return @stat if defined?(@stat) - @stat = Train::Extras::Stat.stat(@spath, @backend, @follow_symlink) - end - - private - - # Returns full path of a symlink target(real dest) or '' on symlink loop - def read_target_path - full_path = @backend.run_command("readlink -n #{@spath} -f").stdout - # Needed for some OSes like OSX that returns relative path - # when the link and target are in the same directory - if !full_path.start_with?('/') && full_path != '' - full_path = File.expand_path("../#{full_path}", @spath) - end - full_path - end - end -end diff --git a/lib/train/extras/file_windows.rb b/lib/train/extras/file_windows.rb deleted file mode 100644 index e276dad8..00000000 --- a/lib/train/extras/file_windows.rb +++ /dev/null @@ -1,100 +0,0 @@ -# encoding: utf-8 -# author: Dominik Richter -# author: Christoph Hartmann - -require 'shellwords' -require 'train/extras/stat' - -# PS C:\Users\Administrator> Get-Item -Path C:\test.txt | Select-Object -Property BaseName, FullName, IsReadOnly, Exists, -# LinkType, Mode, VersionInfo, Owner, Archive, Hidden, ReadOnly, System | ConvertTo-Json - -module Train::Extras - class WindowsFile < FileCommon - attr_reader :path - def initialize(backend, path, follow_symlink = false) - super(backend, path, follow_symlink) - @spath = sanitize_filename(@path) - end - - def basename(suffix = nil, sep = '\\') - super(suffix, sep) - end - - # Ensures we do not use invalid characters for file names - # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions - def sanitize_filename(filename) - return if filename.nil? - # we do not filter :, backslash and forward slash, since they are part of the path - filename.gsub(/[<>"|?*]/, '') - end - - def content - return @content if defined?(@content) - @content = @backend.run_command( - "Get-Content(\"#{@spath}\") | Out-String").stdout - return @content unless @content.empty? - @content = nil if directory? # or size.nil? or size > 0 - @content - end - - def exist? - return @exist if defined?(@exist) - @exist = @backend.run_command( - "(Test-Path -Path \"#{@spath}\").ToString()").stdout.chomp == 'True' - end - - def link_path - nil - end - - def mounted - nil - end - - def owner - owner = @backend.run_command( - "Get-Acl '#{@spath}' | select -expand Owner").stdout.strip - return if owner.empty? - owner - end - - def type - if attributes.include?('Archive') - return :file - elsif attributes.include?('Directory') - return :directory - end - :unknown - end - - %w{ - mode group uid gid mtime size selinux_label - }.each do |field| - define_method field.to_sym do - nil - end - end - - def product_version - @product_version ||= @backend.run_command( - "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").ProductVersion").stdout.chomp - end - - def file_version - @file_version ||= @backend.run_command( - "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").FileVersion").stdout.chomp - end - - def stat - nil - end - - private - - def attributes - return @attributes if defined?(@attributes) - @attributes = @backend.run_command( - "(Get-ItemProperty -Path \"#{@spath}\").attributes.ToString()").stdout.chomp.split(/\s*,\s*/) - end - end -end diff --git a/lib/train/extras/file_common.rb b/lib/train/file.rb similarity index 58% rename from lib/train/extras/file_common.rb rename to lib/train/file.rb index 19481036..045b450d 100644 --- a/lib/train/extras/file_common.rb +++ b/lib/train/file.rb @@ -1,17 +1,39 @@ # encoding: utf-8 -# author: Dominik Richter +# # author: Christoph Hartmann +# author: Dominik Richter +require 'train/file/local' +require 'train/file/local/unix' +require 'train/file/local/windows' +require 'train/file/remote' +require 'train/file/remote/unix' +require 'train/file/remote/linux' +require 'train/file/remote/windows' +require 'train/file/remote/qnx' require 'digest/sha2' require 'digest/md5' +require 'train/extras/stat' + +module Train + class File + def initialize(backend, path, follow_symlink = true) + @backend = backend + @path = path || '' + @follow_symlink = follow_symlink + + sanitize_filename(path) + end + + # This method gets override by particular os class. + def sanitize_filename(_path) + nil + end -module Train::Extras - class FileCommon # rubocop:disable Metrics/ClassLength # interface methods: these fields should be implemented by every # backend File DATA_FIELDS = %w{ exist? mode owner group uid gid content mtime size selinux_label path - product_version file_version }.freeze DATA_FIELDS.each do |m| @@ -20,12 +42,6 @@ class FileCommon # rubocop:disable Metrics/ClassLength end end - def initialize(backend, path, follow_symlink = true) - @backend = backend - @path = path || '' - @follow_symlink = follow_symlink - end - def to_json res = Hash[DATA_FIELDS.map { |x| [x, method(x).call] }] # additional fields provided as input @@ -38,9 +54,6 @@ def type :unknown end - # The following methods can be overwritten by a derived class - # if desired, to e.g. achieve optimizations. - def md5sum res = Digest::MD5.new res.update(content) @@ -57,36 +70,6 @@ def sha256sum nil end - # Additional methods for convenience - - def file? - type.to_s == 'file' - end - - def block_device? - type.to_s == 'block_device' - end - - def character_device? - type.to_s == 'character_device' - end - - def socket? - type.to_s == 'socket' - end - - def directory? - type.to_s == 'directory' - end - - def symlink? - source.type.to_s == 'symlink' - end - - def source_path - @path - end - def source if @follow_symlink self.class.new(@backend, @path, false) @@ -95,28 +78,22 @@ def source end end - def pipe? - type == :pipe - end - - def mode?(sth) - mode == sth - end - - def owned_by?(sth) - owner == sth - end - - def grouped_into?(sth) - group == sth + def source_path + @path end - def linked_to?(dst) - link_path == dst + # product_version is primarily used by Windows operating systems only and will be overwritten + # in Windows-related classes. Since this field is returned for all file objects, the acceptable + # default value is nil + def product_version + nil end - def link_path - symlink? ? path : nil + # file_version is primarily used by Windows operating systems only and will be overwritten + # in Windows-related classes. Since this field is returned for all file objects, the acceptable + # default value is nil + def file_version + nil end def version?(version) @@ -124,48 +101,44 @@ def version?(version) file_version == version end - def unix_mode_mask(owner, type) - o = UNIX_MODE_OWNERS[owner.to_sym] - return nil if o.nil? - - t = UNIX_MODE_TYPES[type.to_sym] - return nil if t.nil? + def block_device? + type.to_s == 'block_device' + end - t & o + def character_device? + type.to_s == 'character_device' end - def mounted? - !mounted.nil? && !mounted.stdout.nil? && !mounted.stdout.empty? + def pipe? + type.to_s == 'pipe' end - def basename(suffix = nil, sep = '/') - fail 'Not yet supported: Suffix in file.basename' unless suffix.nil? - @basename ||= detect_filename(path, sep || '/') + def file? + type.to_s == 'file' end - # helper methods provided to any implementing class + def socket? + type.to_s == 'socket' + end - private + def directory? + type.to_s == 'directory' + end - def detect_filename(path, sep) - idx = path.rindex(sep) - return path if idx.nil? - idx += 1 - return detect_filename(path[0..-2], sep) if idx == path.length - path[idx..-1] + def symlink? + source.type.to_s == 'symlink' end - UNIX_MODE_OWNERS = { - all: 00777, - owner: 00700, - group: 00070, - other: 00007, - }.freeze + def owned_by?(sth) + owner == sth + end - UNIX_MODE_TYPES = { - r: 00444, - w: 00222, - x: 00111, - }.freeze + def path + if symlink? && @follow_symlink + link_path + else + @path + end + end end end diff --git a/lib/train/file/local.rb b/lib/train/file/local.rb new file mode 100644 index 00000000..c500370a --- /dev/null +++ b/lib/train/file/local.rb @@ -0,0 +1,70 @@ +# encoding: utf-8 + +module Train + class File + class Local < Train::File + %w{ + exist? file? socket? directory? symlink? pipe? size basename + }.each do |m| + define_method m.to_sym do + ::File.method(m.to_sym).call(@path) + end + end + + def content + @content ||= ::File.read(@path, encoding: 'UTF-8') + rescue StandardError => _ + nil + end + + def link_path + return nil unless symlink? + begin + @link_path ||= ::File.realpath(@path) + rescue Errno::ELOOP => _ + # Leave it blank on symbolic loop, same as readlink + @link_path = '' + end + end + + def block_device? + ::File.blockdev?(@path) + end + + def character_device? + ::File.chardev?(@path) + end + + def type + case ::File.ftype(@path) + when 'blockSpecial' + :block_device + when 'characterSpecial' + :character_device + when 'link' + :symlink + when 'fifo' + :pipe + else + ::File.ftype(@path).to_sym + end + end + + %w{ + mode owner group uid gid mtime selinux_label + }.each do |field| + define_method field.to_sym do + stat[field.to_sym] + end + end + + def mode?(sth) + mode == sth + end + + def linked_to?(dst) + link_path == dst + end + end + end +end diff --git a/lib/train/file/local/unix.rb b/lib/train/file/local/unix.rb new file mode 100644 index 00000000..d24cd9dc --- /dev/null +++ b/lib/train/file/local/unix.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 + +require 'shellwords' +require 'train/extras/stat' + +module Train + class File + class Local + class Unix < Train::File::Local + def sanitize_filename(path) + @spath = Shellwords.escape(path) || @path + end + + def stat + return @stat if defined?(@stat) + + begin + file_stat = + if @follow_symlink + ::File.stat(@path) + else + ::File.lstat(@path) + end + rescue StandardError => _err + return @stat = {} + end + + @stat = { + type: Train::Extras::Stat.find_type(file_stat.mode), + mode: file_stat.mode & 07777, + mtime: file_stat.mtime.to_i, + size: file_stat.size, + owner: pw_username(file_stat.uid), + uid: file_stat.uid, + group: pw_groupname(file_stat.gid), + gid: file_stat.gid, + } + + lstat = @follow_symlink ? ' -L' : '' + res = @backend.run_command("stat#{lstat} #{@spath} 2>/dev/null --printf '%C'") + if res.exit_status == 0 && !res.stdout.empty? && res.stdout != '?' + @stat[:selinux_label] = res.stdout.strip + end + + @stat + end + + def mounted + @mounted ||= + @backend.run_command("mount | grep -- ' on #{@spath} '") + end + + def mounted? + !mounted.nil? && !mounted.stdout.nil? && !mounted.stdout.empty? + end + + def grouped_into?(sth) + group == sth + end + + private + + def pw_username(uid) + Etc.getpwuid(uid).name + rescue ArgumentError => _ + nil + end + + def pw_groupname(gid) + Etc.getgrgid(gid).name + rescue ArgumentError => _ + nil + end + end + end + end +end diff --git a/lib/train/file/local/windows.rb b/lib/train/file/local/windows.rb new file mode 100644 index 00000000..ef6c123b --- /dev/null +++ b/lib/train/file/local/windows.rb @@ -0,0 +1,63 @@ +# encoding: utf-8 + +module Train + class File + class Local + class Windows < Train::File::Local + # Ensures we do not use invalid characters for file names + # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions + def sanitize_filename(path) + return if path.nil? + # we do not filter :, backslash and forward slash, since they are part of the path + @spath = path.gsub(/[<>"|?*]/, '') + end + + def product_version + @product_version ||= @backend.run_command( + "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").ProductVersion").stdout.chomp + end + + def file_version + @file_version ||= @backend.run_command( + "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").FileVersion").stdout.chomp + end + + def owner + owner = @backend.run_command( + "Get-Acl \"#{@spath}\" | select -expand Owner").stdout.strip + return if owner.empty? + owner + end + + def stat + return @stat if defined?(@stat) + + begin + file_stat = + if @follow_symlink + ::File.stat(@path) + else + ::File.lstat(@path) + end + rescue StandardError => _err + return @stat = {} + end + + @stat = { + type: type, + mode: file_stat.mode, + mtime: file_stat.mtime.to_i, + size: file_stat.size, + owner: owner, + uid: file_stat.uid, + group: nil, + gid: file_stat.gid, + selinux_label: nil, + } + + @stat + end + end + end + end +end diff --git a/lib/train/file/remote.rb b/lib/train/file/remote.rb new file mode 100644 index 00000000..9efb4889 --- /dev/null +++ b/lib/train/file/remote.rb @@ -0,0 +1,28 @@ +# encoding: utf-8 + +module Train + class File + class Remote < Train::File + def basename(suffix = nil, sep = '/') + fail 'Not yet supported: Suffix in file.basename' unless suffix.nil? + @basename ||= detect_filename(path, sep || '/') + end + + def stat + return @stat if defined?(@stat) + @stat = Train::Extras::Stat.stat(@spath, @backend, @follow_symlink) + end + + # helper methods provided to any implementing class + private + + def detect_filename(path, sep) + idx = path.rindex(sep) + return path if idx.nil? + idx += 1 + return detect_filename(path[0..-2], sep) if idx == path.length + path[idx..-1] + end + end + end +end diff --git a/lib/train/file/remote/aix.rb b/lib/train/file/remote/aix.rb new file mode 100644 index 00000000..ddd92bfe --- /dev/null +++ b/lib/train/file/remote/aix.rb @@ -0,0 +1,21 @@ +# encoding: utf-8 + +require 'train/file/remote/unix' + +module Train + class File + class Remote + class Aix < Train::File::Remote::Unix + def link_path + return nil unless symlink? + @link_path ||= + @backend.run_command("perl -e 'print readlink shift' #{@spath}").stdout.chomp + end + + def mounted + @mounted ||= @backend.run_command("lsfs -c #{@spath}") + end + end + end + end +end diff --git a/lib/train/file/remote/linux.rb b/lib/train/file/remote/linux.rb new file mode 100644 index 00000000..dc93de4e --- /dev/null +++ b/lib/train/file/remote/linux.rb @@ -0,0 +1,19 @@ +# encoding: utf-8 + +require 'train/file/remote/unix' + +module Train + class File + class Remote + class Linux < Train::File::Remote::Unix + def content + return @content if defined?(@content) + @content = @backend.run_command("cat #{@path} || echo -n").stdout + return @content unless @content.empty? + @content = nil if directory? or size.nil? or size > 0 + @content + end + end + end + end +end diff --git a/lib/train/file/remote/qnx.rb b/lib/train/file/remote/qnx.rb new file mode 100644 index 00000000..2d5eea9d --- /dev/null +++ b/lib/train/file/remote/qnx.rb @@ -0,0 +1,41 @@ +# encoding: utf-8 +# +# author: Christoph Hartmann +# author: Dominik Richter + +require 'train/file/remote/unix' + +module Train + class File + class Remote + class Aix < Train::File::Remote::Unix + def content + cat = 'cat' + cat = '/proc/boot/cat' if @backend.os[:release].to_i >= 7 + @content ||= case + when !exist? + nil + else + @backend.run_command("#{cat} #{@spath}").stdout || '' + end + end + + def type + if @backend.run_command("file #{@spath}").stdout.include?('directory') + return :directory + else + return :file + end + end + + %w{ + mode owner group uid gid mtime size selinux_label link_path mounted stat + }.each do |field| + define_method field.to_sym do + fail NotImplementedError, "QNX does not implement the #{m}() method yet." + end + end + end + end + end +end diff --git a/lib/train/file/remote/unix.rb b/lib/train/file/remote/unix.rb new file mode 100644 index 00000000..68ac7ff5 --- /dev/null +++ b/lib/train/file/remote/unix.rb @@ -0,0 +1,110 @@ +# encoding: utf-8 + +require 'shellwords' + +module Train + class File + class Remote + class Unix < Train::File::Remote + attr_reader :path + + def sanitize_filename(path) + @spath = Shellwords.escape(path) || @path + end + + def content + @content ||= + if !exist? || directory? + nil + elsif size.nil? || size.zero? + '' + else + @backend.run_command("cat #{@spath}").stdout || '' + end + end + + def exist? + @exist ||= ( + f = @follow_symlink ? '' : " || test -L #{@spath}" + @backend.run_command("test -e #{@spath}"+f) + .exit_status == 0 + ) + end + + def mounted? + !mounted.nil? && !mounted.stdout.nil? && !mounted.stdout.empty? + end + + def mounted + @mounted ||= + @backend.run_command("mount | grep -- ' on #{@spath} '") + end + + %w{ + type mode owner group uid gid mtime size selinux_label + }.each do |field| + define_method field.to_sym do + stat[field.to_sym] + end + end + + def mode?(sth) + mode == sth + end + + def grouped_into?(sth) + group == sth + end + + def linked_to?(dst) + link_path == dst + end + + def link_path + symlink? ? path : nil + end + + def unix_mode_mask(owner, type) + o = UNIX_MODE_OWNERS[owner.to_sym] + return nil if o.nil? + + t = UNIX_MODE_TYPES[type.to_sym] + return nil if t.nil? + + t & o + end + + def path + return @path unless @follow_symlink && symlink? + @link_path ||= read_target_path + end + + private + + # Returns full path of a symlink target(real dest) or '' on symlink loop + def read_target_path + full_path = @backend.run_command("readlink -n #{@spath} -f").stdout + # Needed for some OSes like OSX that returns relative path + # when the link and target are in the same directory + if !full_path.start_with?('/') && full_path != '' + full_path = ::File.expand_path("../#{full_path}", @spath) + end + full_path + end + + UNIX_MODE_OWNERS = { + all: 00777, + owner: 00700, + group: 00070, + other: 00007, + }.freeze + + UNIX_MODE_TYPES = { + r: 00444, + w: 00222, + x: 00111, + }.freeze + end + end + end +end diff --git a/lib/train/file/remote/windows.rb b/lib/train/file/remote/windows.rb new file mode 100644 index 00000000..6e07240b --- /dev/null +++ b/lib/train/file/remote/windows.rb @@ -0,0 +1,94 @@ +# encoding: utf-8 + +module Train + class File + class Remote + class Windows < Train::File::Remote + attr_reader :path + # Ensures we do not use invalid characters for file names + # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions + def sanitize_filename(path) + return if path.nil? + # we do not filter :, backslash and forward slash, since they are part of the path + @spath = path.gsub(/[<>"|?*]/, '') + end + + def basename(suffix = nil, sep = '\\') + super(suffix, sep) + end + + def content + return @content if defined?(@content) + @content = @backend.run_command("Get-Content(\"#{@spath}\") | Out-String").stdout + return @content unless @content.empty? + @content = nil if directory? # or size.nil? or size > 0 + @content + end + + def exist? + return @exist if defined?(@exist) + @exist = @backend.run_command( + "(Test-Path -Path \"#{@spath}\").ToString()").stdout.chomp == 'True' + end + + def owner + owner = @backend.run_command( + "Get-Acl \"#{@spath}\" | select -expand Owner").stdout.strip + return if owner.empty? + owner + end + + def type + if attributes.include?('Archive') + return :file + elsif attributes.include?('ReparsePoint') + return :symlink + elsif attributes.include?('Directory') + return :directory + end + :unknown + end + + def size + if file? + @backend.run_command("((Get-Item '#{@spath}').Length)").stdout.strip.to_i + end + end + + def product_version + @product_version ||= @backend.run_command( + "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").ProductVersion").stdout.chomp + end + + def file_version + @file_version ||= @backend.run_command( + "[System.Diagnostics.FileVersionInfo]::GetVersionInfo(\"#{@spath}\").FileVersion").stdout.chomp + end + + %w{ + mode group uid gid mtime selinux_label + }.each do |field| + define_method field.to_sym do + nil + end + end + + def link_path + nil + end + + def mounted + nil + end + + private + + def attributes + return @attributes if defined?(@attributes) + @attributes = @backend.run_command( + "(Get-ItemProperty -Path \"#{@spath}\").attributes.ToString()").stdout.chomp.split(/\s*,\s*/) + end + end + end + end +end diff --git a/lib/train/plugins/base_connection.rb b/lib/train/plugins/base_connection.rb index e621fcf3..ecf87ac4 100644 --- a/lib/train/plugins/base_connection.rb +++ b/lib/train/plugins/base_connection.rb @@ -6,6 +6,7 @@ require 'train/errors' require 'train/extras' +require 'train/file' require 'logger' class Train::Plugins::Transport diff --git a/lib/train/transports/docker.rb b/lib/train/transports/docker.rb index 15e1b7c6..6af3204b 100644 --- a/lib/train/transports/docker.rb +++ b/lib/train/transports/docker.rb @@ -74,7 +74,14 @@ def os end def file(path) - @files[path] ||= LinuxFile.new(self, path) + @files[path] ||=\ + if os.aix? + Train::File::Remote::Aix.new(self, path) + elsif os.solaris? + Train::File::Remote::Unix.new(self, path) + else + Train::File::Remote::Linux.new(self, path) + end end def run_command(cmd) diff --git a/lib/train/transports/local.rb b/lib/train/transports/local.rb index caf068a0..60aef601 100644 --- a/lib/train/transports/local.rb +++ b/lib/train/transports/local.rb @@ -17,7 +17,6 @@ def connection(_ = nil) end class Connection < BaseConnection - require 'train/transports/local_file' require 'train/transports/local_os' def initialize(options) @@ -40,7 +39,12 @@ def os end def file(path) - @files[path] ||= File.new(self, path) + @files[path] ||= \ + if os.windows? + Train::File::Local::Windows.new(self, path) + else + Train::File::Local::Unix.new(self, path) + end end def login_command diff --git a/lib/train/transports/local_file.rb b/lib/train/transports/local_file.rb deleted file mode 100644 index f789b226..00000000 --- a/lib/train/transports/local_file.rb +++ /dev/null @@ -1,98 +0,0 @@ -# encoding: utf-8 -# -# author: Dominik Richter -# author: Christoph Hartmann - -require 'train/extras' - -class Train::Transports::Local::Connection - class File < Train::Extras::LinuxFile - def content - @content ||= ::File.read(@path, encoding: 'UTF-8') - rescue StandardError => _ - nil - end - - %w{ - exist? file? socket? directory? symlink? pipe? - }.each do |m| - define_method m.to_sym do - ::File.method(m.to_sym).call(@path) - end - end - - def path - if symlink? && @follow_symlink - link_path - else - @path - end - end - - def link_path - return nil unless symlink? - begin - @link_path ||= ::File.realpath(@path) - rescue Errno::ELOOP => _ - # Leave it blank on symbolic loop, same as readlink - @link_path = '' - end - end - - def block_device? - ::File.blockdev?(@path) - end - - def character_device? - ::File.chardev?(@path) - end - - private - - def pw_username(uid) - Etc.getpwuid(uid).name - rescue ArgumentError => _ - nil - end - - def pw_groupname(gid) - Etc.getgrgid(gid).name - rescue ArgumentError => _ - nil - end - - def stat - return @stat if defined? @stat - - begin - file_stat = - if @follow_symlink - ::File.stat(@path) - else - ::File.lstat(@path) - end - rescue StandardError => _err - return @stat = {} - end - - @stat = { - type: Train::Extras::Stat.find_type(file_stat.mode), - mode: file_stat.mode & 07777, - mtime: file_stat.mtime.to_i, - size: file_stat.size, - owner: pw_username(file_stat.uid), - uid: file_stat.uid, - group: pw_groupname(file_stat.gid), - gid: file_stat.gid, - } - - lstat = @follow_symlink ? ' -L' : '' - res = @backend.run_command("stat#{lstat} #{@spath} 2>/dev/null --printf '%C'") - if res.exit_status == 0 && !res.stdout.empty? && res.stdout != '?' - @stat[:selinux_label] = res.stdout.strip - end - - @stat - end - end -end diff --git a/lib/train/transports/mock.rb b/lib/train/transports/mock.rb index 75e59a39..82bb38f2 100644 --- a/lib/train/transports/mock.rb +++ b/lib/train/transports/mock.rb @@ -129,20 +129,20 @@ def detect_family end class Train::Transports::Mock::Connection - class File < FileCommon + class File < Train::File def self.from_json(json) res = new(json['backend'], json['path'], json['follow_symlink']) res.type = json['type'] - Train::Extras::FileCommon::DATA_FIELDS.each do |f| + Train::File::DATA_FIELDS.each do |f| m = (f.tr('?', '') + '=').to_sym res.method(m).call(json[f]) end res end - Train::Extras::FileCommon::DATA_FIELDS.each do |m| + Train::File::DATA_FIELDS.each do |m| attr_accessor m.tr('?', '').to_sym next unless m.include?('?') diff --git a/lib/train/transports/ssh_connection.rb b/lib/train/transports/ssh_connection.rb index 7dc85b5d..7ea8f8cc 100644 --- a/lib/train/transports/ssh_connection.rb +++ b/lib/train/transports/ssh_connection.rb @@ -62,13 +62,13 @@ def os def file(path) @files[path] ||= \ if os.aix? - AixFile.new(self, path) + Train::File::Remote::Aix.new(self, path) elsif os.solaris? - UnixFile.new(self, path) + Train::File::Remote::Unix.new(self, path) elsif os[:name] == 'qnx' - QnxFile.new(self, path) + Train::File::Remote::Qnx.new(self, path) else - LinuxFile.new(self, path) + Train::File::Remote::Linux.new(self, path) end end diff --git a/lib/train/transports/winrm_connection.rb b/lib/train/transports/winrm_connection.rb index cd3e7b66..a9eebd57 100644 --- a/lib/train/transports/winrm_connection.rb +++ b/lib/train/transports/winrm_connection.rb @@ -51,7 +51,7 @@ def os end def file(path) - @files[path] ||= WindowsFile.new(self, path) + @files[path] ||= Train::File::Remote::Windows.new(self, path) end def run_command(command) diff --git a/test/integration/tests/path_block_device_test.rb b/test/integration/tests/path_block_device_test.rb index de52bd3d..8a4da2b2 100644 --- a/test/integration/tests/path_block_device_test.rb +++ b/test/integration/tests/path_block_device_test.rb @@ -64,11 +64,11 @@ end it 'has no product_version' do - file.product_version.must_equal(nil) + file.product_version.must_be_nil end it 'has no file_version' do - file.file_version.must_equal(nil) + file.file_version.must_be_nil end end end diff --git a/test/integration/tests/path_character_device_test.rb b/test/integration/tests/path_character_device_test.rb index acf5326a..fa3aea77 100644 --- a/test/integration/tests/path_character_device_test.rb +++ b/test/integration/tests/path_character_device_test.rb @@ -64,11 +64,11 @@ end it 'has no product_version' do - file.product_version.must_equal(nil) + file.product_version.must_be_nil end it 'has no file_version' do - file.file_version.must_equal(nil) + file.file_version.must_be_nil end end end diff --git a/test/integration/tests/path_file_test.rb b/test/integration/tests/path_file_test.rb index db073380..c4b9ca69 100644 --- a/test/integration/tests/path_file_test.rb +++ b/test/integration/tests/path_file_test.rb @@ -69,11 +69,11 @@ end it 'has no product_version' do - file.product_version.must_equal(nil) + file.product_version.must_be_nil end it 'has no file_version' do - file.file_version.must_equal(nil) + file.file_version.must_be_nil end it 'provides a json representation' do diff --git a/test/integration/tests/path_folder_test.rb b/test/integration/tests/path_folder_test.rb index ed4485fd..03b0240f 100644 --- a/test/integration/tests/path_folder_test.rb +++ b/test/integration/tests/path_folder_test.rb @@ -34,15 +34,15 @@ else it 'has no content' do - file.content.must_equal(nil) + file.content.must_be_nil end it 'has an md5sum' do - file.md5sum.must_equal(nil) + file.md5sum.must_be_nil end it 'has an sha256sum' do - file.sha256sum.must_equal(nil) + file.sha256sum.must_be_nil end end @@ -80,11 +80,11 @@ end it 'has no product_version' do - file.product_version.must_equal(nil) + file.product_version.must_be_nil end it 'has no file_version' do - file.file_version.must_equal(nil) + file.file_version.must_be_nil end end end diff --git a/test/integration/tests/path_missing_test.rb b/test/integration/tests/path_missing_test.rb index 7bfe7c66..4723c2f6 100644 --- a/test/integration/tests/path_missing_test.rb +++ b/test/integration/tests/path_missing_test.rb @@ -72,6 +72,5 @@ it 'has no file_version' do file.file_version.must_be_nil end - end end diff --git a/test/integration/tests/path_pipe_test.rb b/test/integration/tests/path_pipe_test.rb index b1be2a48..5c0d55c6 100644 --- a/test/integration/tests/path_pipe_test.rb +++ b/test/integration/tests/path_pipe_test.rb @@ -66,13 +66,12 @@ file.selinux_label.must_equal(res) end - it 'has no product_version' do - file.product_version.must_equal(nil) + file.product_version.must_be_nil end it 'has no file_version' do - file.file_version.must_equal(nil) + file.file_version.must_be_nil end end end diff --git a/test/integration/tests/path_symlink_test.rb b/test/integration/tests/path_symlink_test.rb index 2d4ea766..0cae34c3 100644 --- a/test/integration/tests/path_symlink_test.rb +++ b/test/integration/tests/path_symlink_test.rb @@ -85,11 +85,11 @@ end it 'has no product_version' do - file.product_version.must_equal(nil) + file.product_version.must_be_nil end it 'has no file_version' do - file.file_version.must_equal(nil) + file.file_version.must_be_nil end end end diff --git a/test/unit/extras/file_common_test.rb b/test/unit/extras/file_common_test.rb deleted file mode 100644 index d7ae3ef6..00000000 --- a/test/unit/extras/file_common_test.rb +++ /dev/null @@ -1,180 +0,0 @@ -# encoding: utf-8 -require 'helper' -require 'securerandom' -require 'train/extras/file_common' - -describe 'file common' do - let(:cls) { Train::Extras::FileCommon } - let(:new_cls) { cls.new(nil, nil, false) } - - def mockup(stubs) - Class.new(cls) do - stubs.each do |k,v| - define_method k.to_sym do - v - end - end - end.new(nil, nil, false) - end - - it 'has the default type of unknown' do - new_cls.type.must_equal :unknown - end - - it 'calculates md5sum from content' do - content = 'hello world' - new_cls.stub :content, content do |i| - i.md5sum.must_equal '5eb63bbbe01eeed093cb22bb8f5acdc3' - end - end - - it 'sets md5sum of nil content to nil' do - new_cls.stub :content, nil do |i| - i.md5sum.must_be_nil - end - end - - it 'calculates md5sum from content' do - content = 'hello world' - new_cls.stub :content, content do |i| - i.sha256sum.must_equal 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' - end - end - - it 'sets sha256sum of nil content to nil' do - new_cls.stub :content, nil do |i| - i.sha256sum.must_be_nil - end - end - - describe 'type' do - it 'recognized type == file' do - fc = mockup(type: :file) - fc.file?.must_equal true - end - - it 'recognized type == block_device' do - fc = mockup(type: :block_device) - fc.block_device?.must_equal true - end - - it 'recognized type == character_device' do - fc = mockup(type: :character_device) - fc.character_device?.must_equal true - end - - it 'recognized type == socket' do - fc = mockup(type: :socket) - fc.socket?.must_equal true - end - - it 'recognized type == directory' do - fc = mockup(type: :directory) - fc.directory?.must_equal true - end - - it 'recognized type == pipe' do - fc = mockup(type: :pipe) - fc.pipe?.must_equal true - end - - it 'recognized type == symlink' do - fc = mockup(type: :symlink) - fc.symlink?.must_equal true - end - end - - describe 'version' do - it 'recognized wrong version' do - fc = mockup(product_version: rand, file_version: rand) - fc.version?(rand).must_equal false - end - - it 'recognized product_version' do - x = rand - fc = mockup(product_version: x, file_version: rand) - fc.version?(x).must_equal true - end - - it 'recognized file_version' do - x = rand - fc = mockup(product_version: rand, file_version: x) - fc.version?(x).must_equal true - end - end - - describe 'unix_mode_mask' do - - let(:fc) { mockup(type: :file) } - - it 'check owner mode calculation' do - fc.unix_mode_mask('owner', 'x').must_equal 0100 - fc.unix_mode_mask('owner', 'w').must_equal 0200 - fc.unix_mode_mask('owner', 'r').must_equal 0400 - end - - it 'check group mode calculation' do - fc.unix_mode_mask('group', 'x').must_equal 0010 - fc.unix_mode_mask('group', 'w').must_equal 0020 - fc.unix_mode_mask('group', 'r').must_equal 0040 - end - - it 'check other mode calculation' do - fc.unix_mode_mask('other', 'x').must_equal 0001 - fc.unix_mode_mask('other', 'w').must_equal 0002 - fc.unix_mode_mask('other', 'r').must_equal 0004 - end - - it 'check all mode calculation' do - fc.unix_mode_mask('all', 'x').must_equal 0111 - fc.unix_mode_mask('all', 'w').must_equal 0222 - fc.unix_mode_mask('all', 'r').must_equal 0444 - end - end - - describe 'basename helper' do - def fc(path) - mockup(type: :file, path: path) - end - - it 'works with an empty path' do - fc('').basename.must_equal '' - end - - it 'separates a simple path (defaults to unix mode)' do - fc('/dir/file').basename.must_equal 'file' - end - - it 'separates a simple path (Unix mode)' do - fc('/dir/file').basename(nil, '/').must_equal 'file' - end - - it 'separates a simple path (Windows mode)' do - fc('C:\dir\file').basename(nil, '\\').must_equal 'file' - end - - it 'identifies a folder name (Unix mode)' do - fc('/dir/file/').basename(nil, '/').must_equal 'file' - end - - it 'identifies a folder name (Windows mode)' do - fc('C:\dir\file\\').basename(nil, '\\').must_equal 'file' - end - - it 'ignores tailing separators (Unix mode)' do - fc('/dir/file///').basename(nil, '/').must_equal 'file' - end - - it 'ignores tailing separators (Windows mode)' do - fc('C:\dir\file\\\\\\').basename(nil, '\\').must_equal 'file' - end - - it 'doesnt work with backward slashes (Unix mode)' do - fc('C:\dir\file').basename(nil, '/').must_equal 'C:\\dir\file' - end - - it 'doesnt work with forward slashes (Windows mode)' do - fc('/dir/file').basename(nil, '\\').must_equal '/dir/file' - end - end -end diff --git a/test/unit/extras/os_detect_linux_test.rb b/test/unit/extras/os_detect_linux_test.rb index fdf99317..bd5a7447 100644 --- a/test/unit/extras/os_detect_linux_test.rb +++ b/test/unit/extras/os_detect_linux_test.rb @@ -125,8 +125,8 @@ def initialize detector.stubs(:fetch_os_release).returns({ 'ID_LIKE' => 'something_else' }) detector.detect_linux_via_config.must_equal(false) - detector.platform[:family].must_equal(nil) - detector.platform[:release].must_equal(nil) + detector.platform[:family].must_be_nil + detector.platform[:release].must_be_nil end end @@ -154,7 +154,7 @@ def initialize describe 'when no os-release data is available' do it 'returns nil' do detector.expects(:get_config).with('/etc/os-release').returns(nil) - detector.fetch_os_release.must_equal(nil) + detector.fetch_os_release.must_be_nil end end diff --git a/test/unit/extras/os_detect_windows_test.rb b/test/unit/extras/os_detect_windows_test.rb index 26a85a83..d84d681e 100644 --- a/test/unit/extras/os_detect_windows_test.rb +++ b/test/unit/extras/os_detect_windows_test.rb @@ -97,7 +97,7 @@ def initialize detector.detect_windows detector.platform[:family].must_equal('windows') detector.platform[:name].must_equal('Windows 4.10.1998') - detector.platform[:arch].must_equal(nil) + detector.platform[:arch].must_be_nil detector.platform[:release].must_equal('4.10.1998') end end diff --git a/test/unit/extras/windows_file_test.rb b/test/unit/extras/windows_file_test.rb deleted file mode 100644 index 03be29da..00000000 --- a/test/unit/extras/windows_file_test.rb +++ /dev/null @@ -1,44 +0,0 @@ -# encoding: utf-8 -require 'helper' -require 'train/transports/mock' -require 'train/extras' - -describe 'file common' do - let(:cls) { Train::Extras::WindowsFile } - let(:backend) { - backend = Train::Transports::Mock.new.connection - backend.mock_os({ family: 'windows' }) - backend - } - - it 'provides the full path' do - cls.new(backend, 'C:\dir\file').path.must_equal 'C:\dir\file' - end - - it 'provides the basename to a unix path' do - cls.new(backend, 'C:\dir\file').basename.must_equal 'file' - end - - it 'provides the full path with whitespace' do - wf = cls.new(backend, 'C:\Program Files\file name') - wf.path.must_equal 'C:\Program Files\file name' - wf.basename.must_equal 'file name' - end - - it 'reads file contents' do - out = rand.to_s - backend.mock_command('Get-Content("path") | Out-String', out) - cls.new(backend, 'path').content.must_equal out - end - - it 'check escaping of invalid chars in path' do - wf = cls.new(nil, nil) - wf.sanitize_filename('c:/test') .must_equal 'c:/test' - wf.sanitize_filename('c:/test directory') .must_equal 'c:/test directory' - %w{ < > " * ?}.each do |char| - wf.sanitize_filename("c:/test#{char}directory") .must_equal 'c:/testdirectory' - end - end - - # TODO: add all missing file tests!! -end diff --git a/test/unit/file/local/unix_test.rb b/test/unit/file/local/unix_test.rb new file mode 100644 index 00000000..e4940856 --- /dev/null +++ b/test/unit/file/local/unix_test.rb @@ -0,0 +1,112 @@ +require 'helper' +require 'train/file/local/unix' +require 'train/transports/mock' +require 'train/transports/local' + +describe Train::File::Local::Unix do + let(:cls) { Train::File::Local::Unix } + + let(:backend) { + backend = Train::Transports::Mock.new.connection + backend.mock_os({ family: 'linux' }) + backend + } + + it 'checks a mounted path' do + backend.mock_command("mount | grep -- ' on /mount/path '", rand.to_s) + cls.new(backend, '/mount/path').mounted?.must_equal true + end + + describe 'file metadata' do + let(:transport) { Train::Transports::Local.new } + let(:connection) { transport.connection } + + let(:stat) { Struct.new(:mode, :size, :mtime, :uid, :gid) } + let(:uid) { rand } + let(:gid) { rand } + let(:statres) { stat.new(00140755, rand, (rand*100).to_i, uid, gid) } + + def meta_stub(method, param, &block) + pwres = Struct.new(:name) + Etc.stub :getpwuid, pwres.new('owner') do + Etc.stub :getgrgid, pwres.new('group') do + File.stub method, param do; yield; end + end + end + end + + it 'recognizes type' do + meta_stub :stat, statres do + connection.file(rand.to_s).stat[:type].must_equal :socket + end + end + + it 'recognizes mode' do + meta_stub :stat, statres do + connection.file(rand.to_s).stat[:mode].must_equal 00755 + end + end + + it 'recognizes mtime' do + meta_stub :stat, statres do + connection.file(rand.to_s).stat[:mtime].must_equal statres.mtime + end + end + + it 'recognizes size' do + meta_stub :stat, statres do + connection.file(rand.to_s).stat[:size].must_equal statres.size + end + end + + it 'recognizes uid' do + meta_stub :stat, statres do + connection.file(rand.to_s).stat[:uid].must_equal uid + end + end + + it 'recognizes gid' do + meta_stub :stat, statres do + connection.file(rand.to_s).stat[:gid].must_equal gid + end + end + + it 'recognizes owner' do + meta_stub :stat, statres do + connection.file(rand.to_s).owner.must_equal 'owner' + end + end + + it 'recognizes group' do + meta_stub :stat, statres do + connection.file(rand.to_s).group.must_equal 'group' + end + end + + it 'grouped_into' do + meta_stub :stat, statres do + connection.file(rand.to_s).grouped_into?('group').must_equal true + end + end + + it 'recognizes selinux label' do + meta_stub :stat, statres do + label = rand.to_s + res = Train::Extras::CommandResult.new(label, nil, 0) + connection.stub :run_command, res do + connection.file(rand.to_s).selinux_label.must_equal label + end + end + end + + it 'recognizes source selinux label' do + meta_stub :lstat, statres do + label = rand.to_s + res = Train::Extras::CommandResult.new(label, nil, 0) + connection.stub :run_command, res do + connection.file(rand.to_s).source.selinux_label.must_equal label + end + end + end + end +end \ No newline at end of file diff --git a/test/unit/file/local/windows_test.rb b/test/unit/file/local/windows_test.rb new file mode 100644 index 00000000..dc410b64 --- /dev/null +++ b/test/unit/file/local/windows_test.rb @@ -0,0 +1,41 @@ +# encoding: utf-8 + +require 'helper' +require 'train/transports/mock' +require 'train/file/local/windows' + +describe 'file common' do + let(:cls) { Train::File::Local::Windows } + let(:backend) { + backend = Train::Transports::Mock.new.connection + backend.mock_os({ family: 'windows' }) + backend + } + + it 'check escaping of invalid chars in path' do + wf = cls.new(nil, nil) + wf.sanitize_filename('c:/test') .must_equal 'c:/test' + wf.sanitize_filename('c:/test directory') .must_equal 'c:/test directory' + %w{ < > " * ?}.each do |char| + wf.sanitize_filename("c:/test#{char}directory") .must_equal 'c:/testdirectory' + end + end + + it 'returns file version' do + out = rand.to_s + backend.mock_command('[System.Diagnostics.FileVersionInfo]::GetVersionInfo("path").FileVersion', out) + cls.new(backend, 'path').file_version.must_equal out + end + + it 'returns product version' do + out = rand.to_s + backend.mock_command('[System.Diagnostics.FileVersionInfo]::GetVersionInfo("path").FileVersion', out) + cls.new(backend, 'path').file_version.must_equal out + end + + it 'returns owner of file' do + out = rand.to_s + backend.mock_command('Get-Acl "path" | select -expand Owner', out) + cls.new(backend, 'path').owner.must_equal out + end +end diff --git a/test/unit/file/local_test.rb b/test/unit/file/local_test.rb new file mode 100644 index 00000000..84d9eb80 --- /dev/null +++ b/test/unit/file/local_test.rb @@ -0,0 +1,110 @@ +require 'helper' +require 'train/transports/local' + +describe Train::File::Local do + let(:transport) { Train::Transports::Local.new } + let(:connection) { transport.connection } + + it 'gets file contents' do + res = rand.to_s + File.stub :read, res do + connection.file(rand.to_s).content.must_equal(res) + end + end + + { + exist?: :exist?, + file?: :file?, + socket?: :socket?, + directory?: :directory?, + symlink?: :symlink?, + pipe?: :pipe?, + character_device?: :chardev?, + block_device?: :blockdev?, + }.each do |method, file_method| + it "checks if file is a #{method}" do + File.stub file_method.to_sym, true do + connection.file(rand.to_s).method(method.to_sym).call.must_equal(true) + end + end + end + + describe '#type' do + it 'returns the type block_device if it is block device' do + File.stub :ftype, "blockSpecial" do + connection.file(rand.to_s).type.must_equal :block_device + end + end + + it 'returns the type character_device if it is character device' do + File.stub :ftype, "characterSpecial" do + connection.file(rand.to_s).type.must_equal :character_device + end + end + + it 'returns the type symlink if it is symlink' do + File.stub :ftype, "link" do + connection.file(rand.to_s).type.must_equal :symlink + end + end + + it 'returns the type file if it is file' do + File.stub :ftype, "file" do + connection.file(rand.to_s).type.must_equal :file + end + end + + it 'returns the type directory if it is block directory' do + File.stub :ftype, "directory" do + connection.file(rand.to_s).type.must_equal :directory + end + end + + it 'returns the type pipe if it is pipe' do + File.stub :ftype, "fifo" do + connection.file(rand.to_s).type.must_equal :pipe + end + end + + it 'returns the type socket if it is socket' do + File.stub :ftype, "socket" do + connection.file(rand.to_s).type.must_equal :socket + end + end + + it 'returns the unknown if not known' do + File.stub :ftype, "unknown" do + connection.file(rand.to_s).type.must_equal :unknown + end + end + end + + describe '#path' do + it 'returns the path if it is not a symlink' do + File.stub :symlink?, false do + filename = rand.to_s + connection.file(filename).path.must_equal filename + end + end + + it 'returns the link_path if it is a symlink' do + File.stub :symlink?, true do + file_obj = connection.file(rand.to_s) + file_obj.stub :link_path, '/path/to/resolved_link' do + file_obj.path.must_equal '/path/to/resolved_link' + end + end + end + end + + describe '#link_path' do + it 'returns file\'s link path' do + out = rand.to_s + File.stub :realpath, out do + File.stub :symlink?, true do + connection.file(rand.to_s).link_path.must_equal out + end + end + end + end +end \ No newline at end of file diff --git a/test/unit/extras/linux_file_test.rb b/test/unit/file/remote/linux_test.rb similarity index 95% rename from test/unit/extras/linux_file_test.rb rename to test/unit/file/remote/linux_test.rb index b09074bf..28993ce4 100644 --- a/test/unit/extras/linux_file_test.rb +++ b/test/unit/file/remote/linux_test.rb @@ -1,15 +1,15 @@ -# encoding: utf-8 require 'helper' +require 'train/transports/local' +require 'train/file/remote/linux' require 'train/transports/mock' -require 'train/extras' -describe 'file common' do - let(:cls) { Train::Extras::LinuxFile } +describe Train::File::Remote::Linux do + let(:cls) { Train::File::Remote::Linux } let(:backend) { backend = Train::Transports::Mock.new.connection backend.mock_os({ family: 'linux' }) backend - } + } def mock_stat(args, out, err = '', code = 0) backend.mock_command( @@ -39,7 +39,7 @@ def mock_stat(args, out, err = '', code = 0) it 'reads file contents' do backend.mock_command('cat path || echo -n', '') mock_stat('-L path', '', 'some error...', 1) - cls.new(backend, 'path').content.must_equal nil + cls.new(backend, 'path').content.must_be_nil end it 'checks for file existance' do @@ -164,4 +164,4 @@ def mock_stat(args, out, err = '', code = 0) f.selinux_label.must_equal 'labels' end end -end +end \ No newline at end of file diff --git a/test/unit/file/remote/unix_test.rb b/test/unit/file/remote/unix_test.rb new file mode 100644 index 00000000..0d75ddb3 --- /dev/null +++ b/test/unit/file/remote/unix_test.rb @@ -0,0 +1,44 @@ +require 'helper' +require 'train/file/remote/unix' + +describe Train::File::Remote::Unix do + let(:cls) { Train::File::Remote::Unix } + + def mockup(stubs) + Class.new(cls) do + stubs.each do |k,v| + define_method k.to_sym do + v + end + end + end.new(nil, nil, false) + end + + describe 'unix_mode_mask' do + let(:fc) { mockup(type: :file) } + + it 'check owner mode calculation' do + fc.unix_mode_mask('owner', 'x').must_equal 0100 + fc.unix_mode_mask('owner', 'w').must_equal 0200 + fc.unix_mode_mask('owner', 'r').must_equal 0400 + end + + it 'check group mode calculation' do + fc.unix_mode_mask('group', 'x').must_equal 0010 + fc.unix_mode_mask('group', 'w').must_equal 0020 + fc.unix_mode_mask('group', 'r').must_equal 0040 + end + + it 'check other mode calculation' do + fc.unix_mode_mask('other', 'x').must_equal 0001 + fc.unix_mode_mask('other', 'w').must_equal 0002 + fc.unix_mode_mask('other', 'r').must_equal 0004 + end + + it 'check all mode calculation' do + fc.unix_mode_mask('all', 'x').must_equal 0111 + fc.unix_mode_mask('all', 'w').must_equal 0222 + fc.unix_mode_mask('all', 'r').must_equal 0444 + end + end +end diff --git a/test/unit/file/remote_test.rb b/test/unit/file/remote_test.rb new file mode 100644 index 00000000..32fed0f3 --- /dev/null +++ b/test/unit/file/remote_test.rb @@ -0,0 +1,62 @@ +require 'helper' +require 'train/file/remote' + +describe Train::File::Remote do + let(:cls) { Train::File::Remote } + + def mockup(stubs) + Class.new(cls) do + stubs.each do |k,v| + define_method k.to_sym do + v + end + end + end.new(nil, nil, false) + end + + describe 'basename helper' do + def fc(path) + mockup(type: :file, path: path) + end + + it 'works with an empty path' do + fc('').basename.must_equal '' + end + + it 'separates a simple path (defaults to unix mode)' do + fc('/dir/file').basename.must_equal 'file' + end + + it 'separates a simple path (Unix mode)' do + fc('/dir/file').basename(nil, '/').must_equal 'file' + end + + it 'separates a simple path (Windows mode)' do + fc('C:\dir\file').basename(nil, '\\').must_equal 'file' + end + + it 'identifies a folder name (Unix mode)' do + fc('/dir/file/').basename(nil, '/').must_equal 'file' + end + + it 'identifies a folder name (Windows mode)' do + fc('C:\dir\file\\').basename(nil, '\\').must_equal 'file' + end + + it 'ignores tailing separators (Unix mode)' do + fc('/dir/file///').basename(nil, '/').must_equal 'file' + end + + it 'ignores tailing separators (Windows mode)' do + fc('C:\dir\file\\\\\\').basename(nil, '\\').must_equal 'file' + end + + it 'doesnt work with backward slashes (Unix mode)' do + fc('C:\dir\file').basename(nil, '/').must_equal 'C:\\dir\file' + end + + it 'doesnt work with forward slashes (Windows mode)' do + fc('/dir/file').basename(nil, '\\').must_equal '/dir/file' + end + end +end \ No newline at end of file diff --git a/test/unit/file_test.rb b/test/unit/file_test.rb new file mode 100644 index 00000000..654dafd0 --- /dev/null +++ b/test/unit/file_test.rb @@ -0,0 +1,156 @@ +require 'helper' + +describe Train::File do + let(:cls) { Train::File } + let(:new_cls) { cls.new(nil, '/temp/file', false) } + + def mockup(stubs) + Class.new(cls) do + stubs.each do |k,v| + define_method k.to_sym do + v + end + end + end.new(nil, nil, false) + end + + it 'has the default type of unknown' do + new_cls.type.must_equal :unknown + end + + it 'calculates md5sum from content' do + content = 'hello world' + new_cls.stub :content, content do |i| + i.md5sum.must_equal '5eb63bbbe01eeed093cb22bb8f5acdc3' + end + end + + it 'sets md5sum of nil content to nil' do + new_cls.stub :content, nil do |i| + i.md5sum.must_be_nil + end + end + + it 'calculates sha256sum from content' do + content = 'hello world' + new_cls.stub :content, content do |i| + i.sha256sum.must_equal 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + end + end + + it 'sets sha256sum of nil content to nil' do + new_cls.stub :content, nil do |i| + i.sha256sum.must_be_nil + end + end + + it 'throws Not implemented error for exist?' do + # proc { Train.validate_backend({ host: rand }) }.must_raise Train::UserError + proc { new_cls.exist?}.must_raise NotImplementedError + end + + it 'throws Not implemented error for mode' do + proc { new_cls.mode }.must_raise NotImplementedError + end + + it 'throws Not implemented error for owner' do + proc { new_cls.owner }.must_raise NotImplementedError + end + + it 'throws Not implemented error for group' do + proc { new_cls.group }.must_raise NotImplementedError + end + + it 'throws Not implemented error for uid' do + proc { new_cls.uid }.must_raise NotImplementedError + end + + it 'throws Not implemented error for gid' do + proc { new_cls.gid }.must_raise NotImplementedError + end + + it 'throws Not implemented error for content' do + proc { new_cls.content }.must_raise NotImplementedError + end + + it 'throws Not implemented error for mtime' do + proc { new_cls.mtime }.must_raise NotImplementedError + end + + it 'throws Not implemented error for size' do + proc { new_cls.size }.must_raise NotImplementedError + end + + it 'throws Not implemented error for selinux_label' do + proc { new_cls.selinux_label }.must_raise NotImplementedError + end + + it 'return path of file' do + new_cls.path.must_equal('/temp/file') + end + + it 'set product_version to nil' do + new_cls.product_version.must_be_nil + end + + it 'set product_version to nil' do + new_cls.file_version.must_be_nil + end + + + describe 'type' do + it 'recognized type == file' do + fc = mockup(type: :file) + fc.file?.must_equal true + end + + it 'recognized type == block_device' do + fc = mockup(type: :block_device) + fc.block_device?.must_equal true + end + + it 'recognized type == character_device' do + fc = mockup(type: :character_device) + fc.character_device?.must_equal true + end + + it 'recognized type == socket' do + fc = mockup(type: :socket) + fc.socket?.must_equal true + end + + it 'recognized type == directory' do + fc = mockup(type: :directory) + fc.directory?.must_equal true + end + + it 'recognized type == pipe' do + fc = mockup(type: :pipe) + fc.pipe?.must_equal true + end + + it 'recognized type == symlink' do + fc = mockup(type: :symlink) + fc.symlink?.must_equal true + end + end + + describe 'version' do + it 'recognized wrong version' do + fc = mockup(product_version: rand, file_version: rand) + fc.version?(rand).must_equal false + end + + it 'recognized product_version' do + x = rand + fc = mockup(product_version: x, file_version: rand) + fc.version?(x).must_equal true + end + + it 'recognized file_version' do + x = rand + fc = mockup(product_version: rand, file_version: x) + fc.version?(x).must_equal true + end + end +end \ No newline at end of file diff --git a/test/unit/plugins/transport_test.rb b/test/unit/plugins/transport_test.rb index 2a846efa..62660e14 100644 --- a/test/unit/plugins/transport_test.rb +++ b/test/unit/plugins/transport_test.rb @@ -86,7 +86,7 @@ def train_class(opts = {}) it 'default option must not be required' do name, plugin = train_class - plugin.default_options[name][:required].must_equal nil + plugin.default_options[name][:required].must_be_nil end it 'can include options from another module' do diff --git a/test/unit/transports/local_file_test.rb b/test/unit/transports/local_file_test.rb deleted file mode 100644 index c0cf3749..00000000 --- a/test/unit/transports/local_file_test.rb +++ /dev/null @@ -1,202 +0,0 @@ -# encoding: utf-8 -# -# author: Dominik Richter -# author: Christoph Hartmann - -require 'helper' -require 'train/transports/local' - -describe 'local file transport' do - let(:transport) { Train::Transports::Local.new } - let(:connection) { transport.connection } - - it 'gets file contents' do - res = rand.to_s - File.stub :read, res do - connection.file(rand.to_s).content.must_equal(res) - end - end - - it 'checks for file existance' do - File.stub :exist?, true do - connection.file(rand.to_s).exist?.must_equal(true) - end - end - - { - exist?: :exist?, - file?: :file?, - socket?: :socket?, - directory?: :directory?, - symlink?: :symlink?, - pipe?: :pipe?, - character_device?: :chardev?, - block_device?: :blockdev?, - }.each do |method, file_method| - it "checks if file is a #{method}" do - File.stub file_method.to_sym, true do - connection.file(rand.to_s).method(method.to_sym).call.must_equal(true) - end - end - end - - it 'checks a file\'s link path' do - out = rand.to_s - File.stub :realpath, out do - File.stub :symlink?, true do - connection.file(rand.to_s).link_path.must_equal out - end - end - end - - describe '#path' do - it 'returns the path if it is not a symlink' do - File.stub :symlink?, false do - filename = rand.to_s - connection.file(filename).path.must_equal filename - end - end - - it 'returns the link_path if it is a symlink' do - File.stub :symlink?, true do - file_obj = connection.file(rand.to_s) - file_obj.stub :link_path, '/path/to/resolved_link' do - file_obj.path.must_equal '/path/to/resolved_link' - end - end - end - end - - describe 'file metadata' do - let(:stat) { Struct.new(:mode, :size, :mtime, :uid, :gid) } - let(:uid) { rand } - let(:gid) { rand } - let(:statres) { stat.new(00140755, rand, (rand*100).to_i, uid, gid) } - - def meta_stub(method, param, &block) - pwres = Struct.new(:name) - Etc.stub :getpwuid, pwres.new('owner') do - Etc.stub :getgrgid, pwres.new('group') do - File.stub method, param do; yield; end - end - end - end - - it 'recognizes type' do - meta_stub :stat, statres do - connection.file(rand.to_s).type.must_equal :socket - end - end - - it 'recognizes mode' do - meta_stub :stat, statres do - connection.file(rand.to_s).mode.must_equal 00755 - end - end - - it 'recognizes mtime' do - meta_stub :stat, statres do - connection.file(rand.to_s).mtime.must_equal statres.mtime - end - end - - it 'recognizes size' do - meta_stub :stat, statres do - connection.file(rand.to_s).size.must_equal statres.size - end - end - - it 'recognizes owner' do - meta_stub :stat, statres do - connection.file(rand.to_s).owner.must_equal 'owner' - end - end - - it 'recognizes uid' do - meta_stub :stat, statres do - connection.file(rand.to_s).uid.must_equal uid - end - end - - it 'recognizes group' do - meta_stub :stat, statres do - connection.file(rand.to_s).group.must_equal 'group' - end - end - - it 'recognizes gid' do - meta_stub :stat, statres do - connection.file(rand.to_s).gid.must_equal gid - end - end - - it 'recognizes selinux label' do - meta_stub :stat, statres do - label = rand.to_s - res = Train::Extras::CommandResult.new(label, nil, 0) - connection.stub :run_command, res do - connection.file(rand.to_s).selinux_label.must_equal label - end - end - end - - it 'recognizes source type' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.type.must_equal :socket - end - end - - it 'recognizes source mode' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.mode.must_equal 00755 - end - end - - it 'recognizes source mtime' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.mtime.must_equal statres.mtime - end - end - - it 'recognizes source size' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.size.must_equal statres.size - end - end - - it 'recognizes source owner' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.owner.must_equal 'owner' - end - end - - it 'recognizes source uid' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.uid.must_equal uid - end - end - - it 'recognizes source owner' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.owner.must_equal 'owner' - end - end - - it 'recognizes source gid' do - meta_stub :lstat, statres do - connection.file(rand.to_s).source.gid.must_equal gid - end - end - - it 'recognizes source selinux label' do - meta_stub :lstat, statres do - label = rand.to_s - res = Train::Extras::CommandResult.new(label, nil, 0) - connection.stub :run_command, res do - connection.file(rand.to_s).source.selinux_label.must_equal label - end - end - end - end - -end diff --git a/test/unit/transports/mock_test.rb b/test/unit/transports/mock_test.rb index 3895660c..62893d8f 100644 --- a/test/unit/transports/mock_test.rb +++ b/test/unit/transports/mock_test.rb @@ -94,11 +94,11 @@ # tests if all fields between the local json and resulting mock file # are equal - JSON = Train.create('local').connection.file(__FILE__).to_json - RES = Train::Transports::Mock::Connection::File.from_json(JSON) + JSON_DATA = Train.create('local').connection.file(__FILE__).to_json + RES = Train::Transports::Mock::Connection::File.from_json(JSON_DATA) %w{ content mode owner group }.each do |f| it "can be initialized from json (field #{f})" do - RES.method(f).call.must_equal JSON[f] + RES.method(f).call.must_equal JSON_DATA[f] end end end diff --git a/test/windows/local_test.rb b/test/windows/local_test.rb index 30c903e4..a6517749 100644 --- a/test/windows/local_test.rb +++ b/test/windows/local_test.rb @@ -6,6 +6,7 @@ require 'minitest/spec' require 'mocha/setup' require 'train' +require 'tempfile' describe 'windows local command' do let(:conn) { @@ -39,6 +40,111 @@ cmd.stderr.must_equal '' end + describe 'file' do + before do + @temp = Tempfile.new('foo') + @temp.write("hello world") + @temp.rewind + end + + let(:file) { conn.file(@temp.path) } + + it 'exists' do + file.exist?.must_equal(true) + end + + it 'is a file' do + file.file?.must_equal(true) + end + + it 'has type :file' do + file.type.must_equal(:file) + end + + it 'has content' do + file.content.must_equal("hello world") + end + + it 'returns basename of file' do + file_name = ::File.basename(@temp) + file.basename.must_equal(file_name) + end + + it 'has owner name' do + file.owner.wont_be_nil + end + + it 'has no group name' do + file.group.must_be_nil + end + + it 'has no mode' do + file.mode.wont_be_nil + end + + it 'has an md5sum' do + file.md5sum.wont_be_nil + end + + it 'has an sha256sum' do + file.sha256sum.wont_be_nil + end + + it 'has no modified time' do + file.mtime.wont_be_nil + end + + it 'has no size' do + file.size.wont_be_nil + end + + it 'has size 11' do + size = ::File.size(@temp) + file.size.must_equal size + end + + it 'has no selinux_label handling' do + file.selinux_label.must_be_nil + end + + it 'has product_version' do + file.product_version.wont_be_nil + end + + it 'has file_version' do + file.file_version.wont_be_nil + end + + it 'provides a json representation' do + j = file.to_json + j.must_be_kind_of Hash + j['type'].must_equal :file + end + + after do + @temp.close + @temp.unlink + end + end + + describe 'file' do + before do + @temp = Tempfile.new('foo bar') + @temp.rewind + end + + let(:file) { conn.file(@temp.path) } + + it 'provides the full path with whitespace for path #{@temp.path}' do + file.path.must_equal @temp.path + end + + after do + @temp.close + @temp.unlink + end + end + after do # close the connection conn.close diff --git a/test/windows/winrm_test.rb b/test/windows/winrm_test.rb index 37140b37..cbfaa5a8 100644 --- a/test/windows/winrm_test.rb +++ b/test/windows/winrm_test.rb @@ -45,6 +45,131 @@ cmd.stderr.must_equal '' end + + describe 'file' do + before do + @temp = Tempfile.new('foo') + @temp.write("hello world") + @temp.rewind + end + + let(:file) { conn.file(@temp.path) } + + it 'exists' do + file.exist?.must_equal(true) + end + + it 'is a file' do + file.file?.must_equal(true) + end + + it 'has type :file' do + file.type.must_equal(:file) + end + + it 'has content' do + # TODO: this shouldn't include newlines that aren't in the original file + file.content.must_equal("hello world\r\n") + end + + it 'has owner name' do + file.owner.wont_be_nil + end + + it 'has no group name' do + file.group.must_be_nil + end + + it 'has no mode' do + file.mode.must_be_nil + end + + it 'has an md5sum' do + file.md5sum.wont_be_nil + end + + it 'has an sha256sum' do + file.sha256sum.wont_be_nil + end + + it 'has no modified time' do + file.mtime.must_be_nil + end + + it 'has no size' do + file.size.wont_be_nil + end + + it 'has size 11' do + size = ::File.size(@temp) + file.size.must_equal size + end + + it 'has no selinux_label handling' do + file.selinux_label.must_be_nil + end + + it 'has product_version' do + file.product_version.wont_be_nil + end + + # TODO: This is not failing in manual testing + # it 'returns basname of file' do + # basename = ::File.basename(@temp) + # file.basename.must_equal basename + # end + + it 'has file_version' do + file.file_version.wont_be_nil + end + + it 'returns nil for mounted' do + file.mounted.must_be_nil + end + + it 'has no link_path' do + file.link_path.must_be_nil + end + + it 'has no uid' do + file.uid.must_be_nil + end + + it 'has no gid' do + file.gid.must_be_nil + end + + it 'provides a json representation' do + j = file.to_json + j.must_be_kind_of Hash + j['type'].must_equal :file + end + + after do + @temp.close + @temp.unlink + end + end + + describe 'file' do + before do + @temp = Tempfile.new('foo bar') + @temp.write("hello world") + @temp.rewind + end + + let(:file) { conn.file(@temp.path) } + + it 'provides the full path with whitespace for path #{@temp.path}' do + file.path.must_equal @temp.path + end + + after do + @temp.close + @temp.unlink + end + end + after do # close the connection conn.close