class Kitchen::Verifier::Nox

Public Instance Methods

call(state) click to toggle source
# File lib/kitchen/verifier/nox.rb, line 36
def call(state)
  create_sandbox
  debug("Detected platform for instance #{instance.name}: #{instance.platform.os_type}. Config's windows setting value: #{config[:windows]}")
  if (ENV['ONLY_DOWNLOAD_ARTEFACTS'] || '') == '1'
    only_download_artefacts = true
  else
    only_download_artefacts = false
  end
  if (ENV['DONT_DOWNLOAD_ARTEFACTS'] || '') == '1'
    dont_download_artefacts = true
  else
    dont_download_artefacts = false
  end
  if only_download_artefacts and dont_download_artefacts
    error_msg = "The environment variables 'ONLY_DOWNLOAD_ARTEFACTS' or 'DONT_DOWNLOAD_ARTEFACTS' cannot be both set to '1'"
    error(error_msg)
    raise ActionFailed, error_msg
  end
  if only_download_artefacts
    info("[#{name}] Only downloading artefacts from instance #{instance.name} with state=#{state}")
  else
    info("[#{name}] Verify on instance #{instance.name} with state=#{state}")
    if ENV['NOX_ENABLE_FROM_FILENAMES']
      config[:enable_filenames] = true
    end

    if config[:enable_filenames] and ENV['CHANGE_TARGET'] and ENV['BRANCH_NAME'] and ENV['FORCE_FULL'] != 'true'
      require 'git'
      repo = Git.open(Dir.pwd)
      config[:from_filenames] = repo.diff("origin/#{ENV['CHANGE_TARGET']}",
                                          "origin/#{ENV['BRANCH_NAME']}").name_status.keys.select{|file| file.end_with?('.py')}
      debug("Populating `from_filenames` with: #{config[:from_filenames]}")
      if config[:windows] && config[:from_filenames].any?
        # On windows, if the changed files list is too big, it will error.
        # Let's then pass an absolute path to a text file which contains the list of changed
        # files, one per line.
        config[:from_filenames_path] = File.join(sandbox_path, config[:from_filenames_basename])
        from_filenames_contents = "#{config[:from_filenames].join('\n')}"
        File.open(config[:from_filenames_path], "w") { |f| f.write from_filenames_contents }
        info("Created #{config[:from_filenames_path]} with contents:\n#{from_filenames_contents}")
      end
    end
  end
  root_path = (config[:windows] ? '%TEMP%\\kitchen' : '/tmp/kitchen')
  if ENV['KITCHEN_TESTS']
    ENV['KITCHEN_TESTS'].split(' ').each{|test| config[:tests].push(test)}
  end

  if ENV['NOX_PASSTHROUGH_OPTS']
    ENV['NOX_PASSTHROUGH_OPTS'].split(' ').each{|opt| config[:passthrough_opts].push(opt)}
  end

  if ENV['NOX_ENV_NAME']
    noxenv = ENV['NOX_ENV_NAME']
  elsif config[:runtests] == true
    noxenv = "runtests-zeromq"
  else
    # Default to pytest-zeromq
    noxenv = "pytest-zeromq"
  end

  # Is the nox env already including the Python version?
  if not noxenv.match(/^(.*)-([\d]{1})(\.([\d]{1}))?$/)
    # Nox env's are not py<python-version> named, they just use the <python-version>
    # Additionally, nox envs are parametrised to enable or disable test coverage
    # So, the line below becomes something like:
    #   runtests-2(coverage=True)
    #   pytest-3(coverage=False)
    suite = instance.suite.name.gsub('py', '').gsub('2', '2.7')
    noxenv = "#{noxenv}-#{suite}"
  end
  noxenv = "#{noxenv}(coverage=#{config[:coverage] ? 'True' : 'False'})"

  if noxenv.include? "pytest"
    tests = config[:tests].join(' ')
    if config[:sys_stats]
      sys_stats = '--sys-stats'
      if not config[:verbose]
        config[:verbose] = true
      end
    else
      sys_stats = ''
    end
  elsif noxenv.include? "runtests"
    tests = config[:tests].collect{|test| "-n #{test}"}.join(' ')
    sys_stats = ''
  end

  if config[:junitxml]
    junitxml = File.join(root_path, config[:testingdir], 'artifacts', 'xml-unittests-output')
    if noxenv.include? "pytest"
      junitxml = "--junitxml=#{File.join(junitxml, "test-results-#{DateTime.now.strftime('%Y%m%d%H%M%S.%L')}.xml")}"
    else
      junitxml = "--xml=#{junitxml}"
    end
  end

  # Be sure to copy the remote artifacts directory to the local machine
  if config[:windows]
    save = {'$env:KitchenTestingDir/artifacts/' => "#{Dir.pwd}"}
  else
    save = {"#{File.join(root_path, config[:testingdir], 'artifacts')}/" => "#{Dir.pwd}"}
  end
  # Hash insert order matters, that's why we define a new one and merge
  # the one from config
  save.merge!(config[:save])

  command = [
    'nox',
    "-f #{File.join(root_path, config[:testingdir], 'noxfile.py')}",
    (config[:windows] ? "--envdir=C:\\Windows\\Temp\\nox" : ""),
    (config[:windows] ? "-e #{noxenv}" : "-e '#{noxenv}'"),
    '--',
    "--output-columns=#{config[:output_columns]}",
    sys_stats,
    (config[:sysinfo] ? '--sysinfo' : ''),
    (config[:junitxml] ? junitxml : ''),
    (config[:verbose] ? '-vv' : '-v'),
    (config[:run_destructive] ? '--run-destructive' : ''),
    config[:passthrough_opts].join(' '),
  ].join(' ')

  if tests.nil? || tests.empty?
    # If we're not targetting specific tests...
    extra_command = []
    if config[:windows]
      extra_command.push("--names-file=#{root_path}\\testing\\tests\\whitelist.txt")
      if config[:from_filenames_path]
          # Add the required command flag for the tests runner
          extra_command.push("--from-filenames=#{root_path}\\testing\\#{config[:from_filenames_basename]}")
      end
    else
      if config[:from_filenames].any?
        extra_command.push("--from-filenames=#{config[:from_filenames].join(',')}")
      end
    end
    command = "#{command} #{extra_command.join(' ')}"
  else
    command = "#{command} #{tests}"
  end

  environment_vars = {}
  if ENV['CI'] || ENV['DRONE'] || ENV['JENKINS_URL']
    environment_vars['CI'] = 1
  end
  # Hash insert order matters, that's why we define a new one and merge
  # the one from config
  environment_vars.merge!(config[:environment_vars])

  # Strip trailing whitespace
  command = command.rstrip

  if config[:windows]
    command = "cmd.exe /c --% \"#{command}\" 2>&1"
  end
  instance.transport.connection(state) do |conn|
    begin
      if config[:windows]
        conn.execute('$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")')
        conn.execute("$env:PythonPath = [Environment]::ExpandEnvironmentVariables(\"#{File.join(root_path, config[:testingdir])}\")")
        conn.execute("[Environment]::SetEnvironmentVariable(\"KitchenTestingDir\", [Environment]::ExpandEnvironmentVariables(\"#{File.join(root_path, config[:testingdir])}\"), \"Machine\")")
        environment_vars.each do |key, value|
          conn.execute("[Environment]::SetEnvironmentVariable(\"#{key}\", \"#{value}\", \"Machine\")")
        end
      else
        command_env = []
        environment_vars.each do |key, value|
          command_env.push("#{key}=#{value}")
        end
        if not command_env.empty?
          command = "env #{command_env.join(' ')} #{command}"
        end
        begin
          conn.execute(sudo("chown -R $USER #{root_path}"))
        rescue => e
          error("Failed to chown #{root_path} :: #{e}")
        end
      end
      if not only_download_artefacts
        if config[:from_filenames_path]
          upload_file_path = "$env:KitchenTestingDir\\#{config[:from_filenames_basename]}"
          info("Uploading #{config[:from_filenames_path]} to #{upload_file_path} on #{instance.to_str}")
          conn.upload(config[:from_filenames_path], "#{upload_file_path}")
        end
        info("Running Command: #{command}")
        conn.execute(sudo(command))
      end
    ensure
      if not dont_download_artefacts
        save.each do |remote, local|
          if config[:windows]
            if config[:zip_windows_artifacts]
              begin
                conn.execute("7z.exe a #{remote}artifacts.zip #{remote}")
              rescue => e
                begin
                  info("7z.exe failed, attempting zip with powershell Compress-Archive")
                  conn.execute("powershell Compress-Archive #{remote} #{remote}artifacts.zip -Force")
                rescue => e2
                  error("Failed to create zip: #{e2}")
                end
              end
            end
          else
            begin
              conn.execute(sudo("chmod -R +r #{remote}"))
            rescue => e
              error("Failed to chown #{remote} :: #{e}")
            end
          end
          begin
            info("Copying #{remote} to #{local}")
            if config[:windows]
              if config[:zip_windows_artifacts]
                conn.download(remote + "artifacts.zip", local + "/artifacts.zip")
                system('unzip -o artifacts.zip')
                system('rm artifacts.zip')
              end
            else
              conn.download(remote, local)
            end
          rescue => e
            error("Failed to copy #{remote} to #{local} :: #{e}")
          end
        end
      end
    end
  end
  if only_download_artefacts
    info("[#{name}] Download artefacts completed.")
  else
    debug("[#{name}] Verify completed.")
  end
ensure
  cleanup_sandbox
end