diff options
Diffstat (limited to 'qa/qa/tools')
-rw-r--r-- | qa/qa/tools/delete_projects.rb | 2 | ||||
-rw-r--r-- | qa/qa/tools/delete_subgroups.rb | 2 | ||||
-rw-r--r-- | qa/qa/tools/delete_test_resources.rb | 85 | ||||
-rw-r--r-- | qa/qa/tools/delete_test_ssh_keys.rb | 2 | ||||
-rw-r--r-- | qa/qa/tools/generate_perf_testdata.rb | 2 | ||||
-rw-r--r-- | qa/qa/tools/initialize_gitlab_auth.rb | 2 | ||||
-rw-r--r-- | qa/qa/tools/knapsack_report.rb | 118 | ||||
-rw-r--r-- | qa/qa/tools/long_running_spec_reporter.rb | 97 | ||||
-rw-r--r-- | qa/qa/tools/reliable_report.rb | 114 | ||||
-rw-r--r-- | qa/qa/tools/revoke_all_personal_access_tokens.rb | 2 | ||||
-rw-r--r-- | qa/qa/tools/test_resource_data_processor.rb | 66 |
11 files changed, 410 insertions, 82 deletions
diff --git a/qa/qa/tools/delete_projects.rb b/qa/qa/tools/delete_projects.rb index 240901eea6f..1f550f035d1 100644 --- a/qa/qa/tools/delete_projects.rb +++ b/qa/qa/tools/delete_projects.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative '../../qa' - # This script deletes all projects directly under a group specified by ENV['TOP_LEVEL_GROUP_NAME'] # Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS # Optional environment variable: TOP_LEVEL_GROUP_NAME (defaults to 'gitlab-qa-sandbox-group') diff --git a/qa/qa/tools/delete_subgroups.rb b/qa/qa/tools/delete_subgroups.rb index bc905fdeadd..11b45365d4c 100644 --- a/qa/qa/tools/delete_subgroups.rb +++ b/qa/qa/tools/delete_subgroups.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative '../../qa' - # This script deletes all subgroups of a group specified by ENV['TOP_LEVEL_GROUP_NAME'] # Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS # Optional environment variable: TOP_LEVEL_GROUP_NAME (defaults to 'gitlab-qa-sandbox-group') diff --git a/qa/qa/tools/delete_test_resources.rb b/qa/qa/tools/delete_test_resources.rb new file mode 100644 index 00000000000..917cb2fa992 --- /dev/null +++ b/qa/qa/tools/delete_test_resources.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +# This script reads from test_resources.txt file to collect data about resources to delete +# Deletes all deletable resources that E2E tests created +# Resource type: Sandbox, User, Fork and RSpec::Mocks::Double are not included +# +# Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS +# When in CI also requires: QA_TEST_RESOURCES_FILE_PATTERN +# Run `rake delete_test_resources[<file_pattern>]` + +module QA + module Tools + class DeleteTestResources + include Support::API + + def initialize(file_pattern = nil) + raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] + raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN'] + + @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN']) + @file_pattern = file_pattern + end + + def run + puts 'Deleting test created resources...' + + if Runtime::Env.running_in_ci? + raise ArgumentError, 'Please provide QA_TEST_RESOURCES_FILE_PATTERN' unless ENV['QA_TEST_RESOURCES_FILE_PATTERN'] + + Dir.glob(@file_pattern).each do |file| + delete_resources(load_file(file)) + end + else + file = Runtime::Env.test_resources_created_filepath + raise ArgumentError, "'#{file}' either does not exist or empty." if !File.exist?(file) || File.zero?(file) + + delete_resources(load_file(file)) + end + + puts "\nDone" + end + + private + + def load_file(json) + JSON.parse(File.read(json)) + end + + def delete_resources(resources) + failures = [] + + resources.each_key do |type| + next if resources[type].empty? + + resources[type].each do |resource| + next if resource_not_found?(resource['api_path']) + + msg = resource['info'] ? "#{type} - #{resource['info']}" : "#{type} at #{resource['api_path']}" + + puts "\nDeleting #{msg}..." + delete_response = delete(Runtime::API::Request.new(@api_client, resource['api_path']).url) + + if delete_response.code == 202 + print "\e[32m.\e[0m" + else + print "\e[31mF\e[0m" + failures << msg + end + end + end + + unless failures.empty? + puts "\nFailed to delete #{failures.length} resources:\n" + puts failures + end + end + + def resource_not_found?(api_path) + get_response = get Runtime::API::Request.new(@api_client, api_path).url + + get_response.code.eql? 404 + end + end + end +end diff --git a/qa/qa/tools/delete_test_ssh_keys.rb b/qa/qa/tools/delete_test_ssh_keys.rb index 58ab4865336..9e5728a5509 100644 --- a/qa/qa/tools/delete_test_ssh_keys.rb +++ b/qa/qa/tools/delete_test_ssh_keys.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative '../../qa' - # This script deletes all selected test ssh keys for a specific user # Keys can be selected by a string matching part of the key's title and by created date # - Specify `title_portion` to delete only keys that include the string provided diff --git a/qa/qa/tools/generate_perf_testdata.rb b/qa/qa/tools/generate_perf_testdata.rb index 8e5da94e7e6..0f06fd2fbc4 100644 --- a/qa/qa/tools/generate_perf_testdata.rb +++ b/qa/qa/tools/generate_perf_testdata.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'yaml' -require_relative '../../qa' + # This script generates testdata for Performance Testing. # Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS # This job creates a urls.txt which contains a hash of all the URLs needed for Performance Testing diff --git a/qa/qa/tools/initialize_gitlab_auth.rb b/qa/qa/tools/initialize_gitlab_auth.rb index 3ead8fc9bd4..86791f1f624 100644 --- a/qa/qa/tools/initialize_gitlab_auth.rb +++ b/qa/qa/tools/initialize_gitlab_auth.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative '../../qa' - module QA module Tools # Task to set default password from Runtime::Env.default_password if not set already diff --git a/qa/qa/tools/knapsack_report.rb b/qa/qa/tools/knapsack_report.rb index fb405e82e83..e50c4fe63d2 100644 --- a/qa/qa/tools/knapsack_report.rb +++ b/qa/qa/tools/knapsack_report.rb @@ -5,50 +5,86 @@ require "fog/google" module QA module Tools class KnapsackReport + extend SingleForwardable + PROJECT = "gitlab-qa-resources" BUCKET = "knapsack-reports" + FALLBACK_REPORT = "knapsack/master_report.json" - class << self - def download - new.download_report - end + def_delegators :new, :configure!, :move_regenerated_report, :download_report, :upload_report - def upload(glob) - new.upload_report(glob) - end - end + # Configure knapsack report + # + # * Setup variables + # * Fetch latest report + # + # @return [void] + def configure! + ENV["KNAPSACK_TEST_FILE_PATTERN"] ||= "qa/specs/features/**/*_spec.rb" + ENV["KNAPSACK_REPORT_PATH"] = report_path - def initialize - ENV["KNAPSACK_REPORT_PATH"] || raise("KNAPSACK_REPORT_PATH env var is required!") - ENV["QA_KNAPSACK_REPORT_GCS_CREDENTIALS"] || raise("QA_KNAPSACK_REPORT_GCS_CREDENTIALS env var is required!") + Knapsack.logger = QA::Runtime::Logger.logger + + download_report end # Download knapsack report from gcs bucket # # @return [void] def download_report - logger.info("Downloading latest knapsack report '#{report_file}'") + logger.debug("Downloading latest knapsack report for '#{report_name}' to '#{report_path}'") file = client.get_object(BUCKET, report_file) - - logger.info("Saving latest knapsack report to '#{report_path}'") File.write(report_path, file[:body]) + rescue StandardError => e + ENV["KNAPSACK_REPORT_PATH"] = FALLBACK_REPORT + logger.warn("Failed to fetch latest knapsack report: #{e}") + logger.warn("Falling back to '#{FALLBACK_REPORT}'") + end + + # Rename and move new regenerated report to a separate folder used to indicate report name + # + # @return [void] + def move_regenerated_report + return unless ENV["KNAPSACK_GENERATE_REPORT"] == "true" + + tmp_path = "tmp/knapsack/#{report_name}" + FileUtils.mkdir_p(tmp_path) + + # Use path from knapsack config in case of fallback to master_report.json + knapsack_report_path = Knapsack.report.report_path + logger.debug("Moving regenerated #{knapsack_report_path} to save as artifact") + FileUtils.cp(knapsack_report_path, "#{tmp_path}/#{ENV['CI_NODE_INDEX']}.json") end # Merge and upload knapsack report to gcs bucket # + # Fetches all files defined in glob and uses parent folder as report name + # # @param [String] glob # @return [void] def upload_report(glob) - reports = Dir[glob] - return logger.error("Pattern '#{glob}' did not match any files!") if reports.empty? + reports = Pathname.glob(glob).each_with_object(Hash.new { |hsh, key| hsh[key] = [] }) do |report, hash| + next unless report.extname == ".json" + + hash[report.parent.basename.to_s].push(report) + end + return logger.error("Glob '#{glob}' did not contain any valid report files!") if reports.empty? - report = reports - .map { |path| JSON.parse(File.read(path)) } - .reduce({}, :merge) - return logger.error("Knapsack generated empty report, skipping upload!") if report.empty? + reports.each do |name, jsons| + file = "#{name}.json" - logger.info("Uploading latest knapsack report '#{report_file}'") - client.put_object(BUCKET, report_file, JSON.pretty_generate(report)) + report = jsons + .map { |json| JSON.parse(File.read(json)) } + .reduce({}, :merge) + .sort_by { |k, v| v } # sort report by execution time + .to_h + next logger.warn("Knapsack generated empty report for '#{name}', skipping upload!") if report.empty? + + logger.info("Uploading latest knapsack report '#{file}'") + client.put_object(BUCKET, file, JSON.pretty_generate(report)) + rescue StandardError => e + logger.error("Failed to upload knapsack report for '#{name}'. Error: #{e}") + end end private @@ -64,24 +100,50 @@ module QA # # @return [Fog::Storage::GoogleJSON] def client - @client ||= Fog::Storage::Google.new( - google_project: PROJECT, - google_json_key_location: ENV["QA_KNAPSACK_REPORT_GCS_CREDENTIALS"] - ) + @client ||= Fog::Storage::Google.new(google_project: PROJECT, **gcs_credentials) + end + + # Base path of knapsack report + # + # @return [String] + def report_base_path + @report_base_path ||= "knapsack" end # Knapsack report path # # @return [String] def report_path - @report_path ||= ENV["KNAPSACK_REPORT_PATH"] + @report_path ||= "#{report_base_path}/#{report_file}" end # Knapsack report name # # @return [String] def report_file - @report_name ||= report_path.split("/").last + @report_file ||= "#{report_name}.json" + end + + # Report name + # + # Infer report name from ci job name + # Remove characters incompatible with gcs bucket naming from job names like ee:instance-parallel + # + # @return [String] + def report_name + @report_name ||= ENV["CI_JOB_NAME"].split(" ").first.tr(":", "-") + end + + # GCS credentials json + # + # @return [Hash] + def gcs_credentials + json_key = ENV["QA_KNAPSACK_REPORT_GCS_CREDENTIALS"] || raise( + "QA_KNAPSACK_REPORT_GCS_CREDENTIALS env variable is required!" + ) + return { google_json_key_location: json_key } if File.exist?(json_key) + + { google_json_key_string: json_key } end end end diff --git a/qa/qa/tools/long_running_spec_reporter.rb b/qa/qa/tools/long_running_spec_reporter.rb new file mode 100644 index 00000000000..ce035248baa --- /dev/null +++ b/qa/qa/tools/long_running_spec_reporter.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require "fog/google" +require "slack-notifier" + +module QA + module Tools + class LongRunningSpecReporter + extend SingleForwardable + + SLACK_CHANNEL = "#quality-reports" + PROJECT = "gitlab-qa-resources" + BUCKET = "knapsack-reports" + REPORT_NAME = "ee-instance-parallel.json" + RUNTIME_THRESHOLD = 300 + + def_delegator :new, :execute + + # Find and report specs exceeding runtime threshold + # + # @return [void] + def execute + return puts("No long running specs detected, all good!") if long_running_specs.empty? + + specs = long_running_specs.map { |k, v| "#{k}: #{(v / 60).round(2)} minutes" }.join("\n") + average = mean_runtime < 60 ? "#{mean_runtime.round(0)} seconds" : "#{(mean_runtime / 60).round(2)} minutes" + msg = <<~MSG + Following spec files are exceeding #{RUNTIME_THRESHOLD / 60} minute runtime threshold! + Current average spec runtime: #{average}. + MSG + + puts("#{msg}\n#{specs}") + notifier.post(icon_emoji: ":time-out:", text: "#{msg}\n```#{specs}```") + end + + private + + # Average runtime of spec files + # + # @return [Number] + def mean_runtime + @mean_runtime ||= latest_report.values + .select { |v| v < RUNTIME_THRESHOLD } + .yield_self { |runtimes| runtimes.sum(0.0) / runtimes.length } + end + + # Spec files exceeding runtime threshold + # + # @return [Hash] + def long_running_specs + @long_running_specs ||= latest_report.select { |k, v| v > RUNTIME_THRESHOLD } + end + + # Latest knapsack report + # + # @return [Hash] + def latest_report + @latest_report ||= JSON.parse(client.get_object(BUCKET, REPORT_NAME)[:body]) + end + + # Slack notifier + # + # @return [Slack::Notifier] + def notifier + @notifier ||= Slack::Notifier.new( + slack_webhook_url, + channel: SLACK_CHANNEL, + username: "Spec Runtime Report" + ) + end + + # GCS client + # + # @return [Fog::Storage::GoogleJSON] + def client + @client ||= Fog::Storage::Google.new( + google_project: PROJECT, + **(File.exist?(gcs_json) ? { google_json_key_location: gcs_json } : { google_json_key_string: gcs_json }) + ) + end + + # Slack webhook url + # + # @return [String] + def slack_webhook_url + @slack_webhook_url ||= ENV["SLACK_WEBHOOK"] || raise("Missing SLACK_WEBHOOK env variable") + end + + # GCS credentials json + # + # @return [Hash] + def gcs_json + ENV["QA_KNAPSACK_REPORT_GCS_CREDENTIALS"] || raise("Missing QA_KNAPSACK_REPORT_GCS_CREDENTIALS env variable!") + end + end + end +end diff --git a/qa/qa/tools/reliable_report.rb b/qa/qa/tools/reliable_report.rb index 40a452be36e..b99b97c1ea6 100644 --- a/qa/qa/tools/reliable_report.rb +++ b/qa/qa/tools/reliable_report.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative "../../qa" - require "influxdb-client" require "terminal-table" require "slack-notifier" @@ -16,7 +14,7 @@ module QA PROJECT_ID = 278964 def initialize(range) - @range = range + @range = range.to_i @influxdb_bucket = "e2e-test-stats" @slack_channel = "#quality-reports" @influxdb_url = ENV["QA_INFLUXDB_URL"] || raise("Missing QA_INFLUXDB_URL env variable") @@ -34,9 +32,8 @@ module QA reporter.print_report reporter.report_in_issue_and_slack if report_in_issue_and_slack == "true" rescue StandardError => e - puts "Report creation failed! Error: '#{e}'".colorize(:red) - reporter.notify_failure(e) - exit(1) + reporter&.notify_failure(e) + raise(e) end # Print top stable specs @@ -58,7 +55,11 @@ module QA puts "Creating report".colorize(:green) response = post( "#{gitlab_api_url}/projects/#{PROJECT_ID}/issues", - { title: "Reliable spec report", description: report_issue_body, labels: "Quality,test" }, + { + title: "Reliable e2e test report", + description: report_issue_body, + labels: "Quality,test,type::maintenance,reliable test report" + }, headers: { "PRIVATE-TOKEN" => gitlab_access_token } ) web_url = parse_body(response)[:web_url] @@ -96,68 +97,79 @@ module QA # # @return [String] def report_issue_body + execution_interval = "(#{Date.today - range} - #{Date.today})" + issue = [] issue << "[[_TOC_]]" - issue << "# Candidates for promotion to reliable\n\n```\n#{stable_summary_table}\n```" - issue << results_markdown(stable_results_tables) + issue << "# Candidates for promotion to reliable #{execution_interval}" + issue << "Total amount: **#{stable_test_runs.sum { |_k, v| v.count }}**" + issue << stable_summary_table(markdown: true).to_s + issue << results_markdown(:stable) return issue.join("\n\n") if unstable_reliable_test_runs.empty? - issue << "# Reliable specs with failures\n\n```\n#{unstable_summary_table}\n```" - issue << results_markdown(unstable_reliable_results_tables) + issue << "# Reliable specs with failures #{execution_interval}" + issue << "Total amount: **#{unstable_reliable_test_runs.sum { |_k, v| v.count }}**" + issue << unstable_summary_table(markdown: true).to_s + issue << results_markdown(:unstable) issue.join("\n\n") end # Stable spec summary table # + # @param [Boolean] markdown # @return [Terminal::Table] - def stable_summary_table - @stable_summary_table ||= terminal_table( + def stable_summary_table(markdown: false) + terminal_table( rows: stable_test_runs.map { |stage, specs| [stage, specs.length] }, title: "Stable spec summary for past #{range} days".ljust(50), - headings: %w[STAGE COUNT] + headings: %w[STAGE COUNT], + markdown: markdown ) end # Unstable reliable summary table # + # @param [Boolean] markdown # @return [Terminal::Table] - def unstable_summary_table - @unstable_summary_table ||= terminal_table( + def unstable_summary_table(markdown: false) + terminal_table( rows: unstable_reliable_test_runs.map { |stage, specs| [stage, specs.length] }, title: "Unstable spec summary for past #{range} days".ljust(50), - headings: %w[STAGE COUNT] + headings: %w[STAGE COUNT], + markdown: markdown ) end # Result tables for stable specs # + # @param [Boolean] markdown # @return [Hash] - def stable_results_tables - @stable_results ||= results_tables(:stable) + def stable_results_tables(markdown: false) + results_tables(:stable, markdown: markdown) end # Result table for unstable specs # + # @param [Boolean] markdown # @return [Hash] - def unstable_reliable_results_tables - @unstable_results ||= results_tables(:unstable) + def unstable_reliable_results_tables(markdown: false) + results_tables(:unstable, markdown: markdown) end # Markdown formatted tables # - # @param [Hash] results + # @param [Symbol] type result type - :stable, :unstable # @return [String] - def results_markdown(results) - results.map do |stage, table| + def results_markdown(type) + runs = type == :stable ? stable_test_runs : unstable_reliable_test_runs + results_tables(type, markdown: true).map do |stage, table| <<~STAGE.strip - ## #{stage} + ## #{stage} (#{runs[stage].count}) <details> <summary>Executions table</summary> - ``` #{table} - ``` </details> STAGE @@ -167,15 +179,19 @@ module QA # Results table # # @param [Symbol] type result type - :stable, :unstable + # @param [Boolean] markdown # @return [Hash<Symbol, Terminal::Table>] - def results_tables(type) + def results_tables(type, markdown: false) (type == :stable ? stable_test_runs : unstable_reliable_test_runs).to_h do |stage, specs| headings = ["name", "runs", "failures", "failure rate"] [stage, terminal_table( - rows: specs.map { |k, v| [name_column(k, v[:file]), *table_params(v.values)] }, title: "Top #{type} specs in '#{stage}' stage for past #{range} days", - headings: headings.map(&:upcase) + headings: headings.map(&:upcase), + markdown: markdown, + rows: specs.map do |k, v| + [name_column(name: k, file: v[:file], markdown: markdown), *table_params(v.values)] + end )] end end @@ -214,13 +230,17 @@ module QA # Terminal table for result formatting # + # @param [Array] rows + # @param [Array] headings + # @param [String] title + # @param [Boolean] markdown # @return [Terminal::Table] - def terminal_table(rows:, headings:, title: nil) + def terminal_table(rows:, headings:, title:, markdown:) Terminal::Table.new( headings: headings, - style: { all_separators: true }, - title: title, - rows: rows + title: markdown ? nil : title, + rows: rows, + style: markdown ? { border: :markdown } : { all_separators: true } ) end @@ -232,17 +252,17 @@ module QA [*parameters[1..2], "#{parameters.last}%"] end - # Name column value + # Name column content # # @param [String] name # @param [String] file + # @param [Boolean] markdown # @return [String] - def name_column(name, file) - spec_name = name.length > 150 ? "#{name} ".scan(/.{1,150} /).map(&:strip).join("\n") : name - name_line = "name: '#{spec_name}'" - file_line = "file: '#{file}'" + def name_column(name:, file:, markdown: false) + return "**name**: #{name}<br>**file**: #{file}" if markdown - "#{name_line}\n#{file_line.ljust(160)}" + wrapped_name = name.length > 150 ? "#{name} ".scan(/.{1,150} /).map(&:strip).join("\n") : name + "name: '#{wrapped_name}'\nfile: #{file.ljust(160)}" end # Test executions grouped by name @@ -254,10 +274,16 @@ module QA all_runs = query_api.query(query: query(reliable)).values all_runs.each_with_object(Hash.new { |hsh, key| hsh[key] = {} }) do |table, result| - records = table.records - name = records.last.values["name"] - file = records.last.values["file_path"].split("/").last - stage = records.last.values["stage"] || "unknown" + records = table.records.sort_by { |record| record.values["_time"] } + # skip specs that executed less time than defined by range or stopped executing before report date + # offset 1 day due to how schedulers are configured and first run can be 1 day later + next if (Date.today - Date.parse(records.first.values["_time"])).to_i < (range - 1) + next if (Date.today - Date.parse(records.last.values["_time"])).to_i > 1 + + last_record = records.last.values + name = last_record["name"] + file = last_record["file_path"].split("/").last + stage = last_record["stage"] || "unknown" runs = records.count failed = records.count { |r| r.values["status"] == "failed" } diff --git a/qa/qa/tools/revoke_all_personal_access_tokens.rb b/qa/qa/tools/revoke_all_personal_access_tokens.rb index c0a1697fa16..b4fa02a36d4 100644 --- a/qa/qa/tools/revoke_all_personal_access_tokens.rb +++ b/qa/qa/tools/revoke_all_personal_access_tokens.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative '../../qa' require 'net/protocol' + # This script revokes all personal access tokens with the name of 'api-test-token' on the host specified by GITLAB_ADDRESS # Required environment variables: GITLAB_USERNAME, GITLAB_PASSWORD and GITLAB_ADDRESS # Run `rake revoke_personal_access_tokens` diff --git a/qa/qa/tools/test_resource_data_processor.rb b/qa/qa/tools/test_resource_data_processor.rb new file mode 100644 index 00000000000..78fb6ef6cd0 --- /dev/null +++ b/qa/qa/tools/test_resource_data_processor.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# This script collects all resources created during each test execution +# Save the data and write it to a JSON file at the end of suite + +module QA + module Tools + class TestResourceDataProcessor + @resources ||= Hash.new { |hsh, key| hsh[key] = [] } + + class << self + # Ignoring rspec-mocks, sandbox, user and fork resources + # TODO: Will need to figure out which user resources can be collected, ignore for now + # + # Collecting resources created in E2E tests + # Data is a Hash of resources with keys as resource type (group, project, issue, etc.) + # Each type contains an array of resource object (hash) of the same type + # E.g: { "QA::Resource::Project": [ { info: 'foo', api_path: '/foo'}, {...} ] } + def collect(resource, info) + return if resource.api_response.nil? || + resource.is_a?(RSpec::Mocks::Double) || + resource.is_a?(Resource::Sandbox) || + resource.is_a?(Resource::User) || + resource.is_a?(Resource::Fork) + + api_path = if resource.respond_to?(:api_delete_path) + resource.api_delete_path.gsub('%2F', '/') + elsif resource.respond_to?(:api_get_path) + resource.api_get_path.gsub('%2F', '/') + else + 'Cannot find resource API path' + end + + type = resource.class.name + + @resources[type] << { info: info, api_path: api_path } + end + + # If JSON file exists and not empty, read and load file content + # Merge what is saved in @resources into the content from file + # Overwrite file content with the new data hash + # Otherwise create file and write data hash to file for the first time + def write_to_file + return if @resources.empty? + + file = Runtime::Env.test_resources_created_filepath + FileUtils.mkdir_p('tmp/') + FileUtils.touch(file) + data = nil + + if File.zero?(file) + data = @resources + else + data = JSON.parse(File.read(file)) + + @resources.each_pair do |key, val| + data[key].nil? ? data[key] = val : val.each { |item| data[key] << item } + end + end + + File.open(file, 'w') { |f| f.write(JSON.pretty_generate(data.each_value(&:uniq!))) } + end + end + end + end +end |