Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/scripts')
-rw-r--r--spec/scripts/duo_chat/reporter_spec.rb270
-rw-r--r--spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb6
-rw-r--r--spec/scripts/internal_events/cli_spec.rb866
-rw-r--r--spec/scripts/lib/glfm/update_specification_spec.rb3
-rw-r--r--spec/scripts/trigger-build_spec.rb60
5 files changed, 1195 insertions, 10 deletions
diff --git a/spec/scripts/duo_chat/reporter_spec.rb b/spec/scripts/duo_chat/reporter_spec.rb
new file mode 100644
index 00000000000..836c41273e8
--- /dev/null
+++ b/spec/scripts/duo_chat/reporter_spec.rb
@@ -0,0 +1,270 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'gitlab'
+require 'json'
+require_relative '../../../scripts/duo_chat/reporter'
+
+RSpec.describe Reporter, feature_category: :ai_abstraction_layer do
+ subject(:reporter) { described_class.new }
+
+ describe '#run', :freeze_time do
+ let(:ci_commit_sha) { 'commitsha' }
+ let(:ci_pipeline_url) { 'https://gitlab.com/pipeline/url' }
+ let(:client) { double }
+
+ before do
+ stub_env('CI_COMMIT_SHA', ci_commit_sha)
+ stub_env('CI_PIPELINE_URL', ci_pipeline_url)
+ stub_env('CI_COMMIT_BRANCH', ci_commit_branch)
+ stub_env('CI_DEFAULT_BRANCH', ci_default_branch)
+
+ allow(Gitlab).to receive(:client).and_return(client)
+ end
+
+ context 'when the CI pipeline is running with the commit in `master` branch' do
+ let(:ci_commit_branch) { 'master' }
+ let(:ci_default_branch) { 'master' }
+ let(:snippet_web_url) { 'https://gitlab.com/snippet/url' }
+ let(:issue_web_url) { 'https://gitlab.com/issue/url' }
+
+ let(:mock_data) do
+ [
+ {
+ "question" => "question1",
+ "resource" => "resource",
+ "answer" => "answer1",
+ "tools_used" => ["foobar tool"],
+ "evaluations" => [
+ { "model" => "claude-2", "response" => "Grade: CORRECT" },
+ { "model" => "text-bison", "response" => "Grade: CORRECT" }
+ ]
+ }
+ ]
+ end
+
+ before do
+ allow(reporter).to receive(:report_data).and_return(mock_data)
+ end
+
+ it 'uploads snippet, creates a report issue and updates the tracking issue' do
+ # Uploads the test data as a snippet along with commit sha and pipeline url
+ snippet = double(web_url: snippet_web_url) # rubocop: disable RSpec/VerifiedDoubles -- an internal detail of Gitlab gem.
+ snippet_content = ::JSON.pretty_generate({
+ commit: ci_commit_sha,
+ pipeline_url: ci_pipeline_url,
+ data: mock_data
+ })
+
+ expect(client).to receive(:create_snippet).with(
+ described_class::QA_EVALUATION_PROJECT_ID,
+ {
+ title: Time.now.utc.to_s,
+ files: [{ file_path: "#{Time.now.utc.to_i}.json", content: snippet_content }],
+ visibility: 'private'
+ }
+ ).and_return(snippet)
+
+ # Create a new issue for the report
+ issue_title = "Report #{Time.now.utc}"
+ issue = double(web_url: issue_web_url) # rubocop: disable RSpec/VerifiedDoubles -- an internal detail of Gitlab gem.
+
+ expect(client).to receive(:create_issue).with(
+ described_class::QA_EVALUATION_PROJECT_ID,
+ issue_title,
+ { description: reporter.markdown_report }
+ ).and_return(issue)
+
+ # Updates the tracking issue by adding a row that links to the snippet and the issue just created.
+ aggregated_report_issue = double(description: "") # rubocop: disable RSpec/VerifiedDoubles -- an internal detail of Gitlab gem.
+ allow(client).to receive(:issue).with(
+ described_class::QA_EVALUATION_PROJECT_ID,
+ described_class::AGGREGATED_REPORT_ISSUE_IID
+ ).and_return(aggregated_report_issue)
+ row = "\n| #{Time.now.utc} | 1 | 100.0% | 0.0% | 0.0%"
+ row << " | #{issue_web_url} | #{snippet_web_url} |"
+
+ expect(client).to receive(:edit_issue).with(
+ described_class::QA_EVALUATION_PROJECT_ID,
+ described_class::AGGREGATED_REPORT_ISSUE_IID,
+ { description: aggregated_report_issue.description + row }
+ )
+
+ reporter.run
+ end
+ end
+
+ context 'when the CI pipeline is not running with the commit in `master` branch' do
+ let(:ci_commit_branch) { 'foobar' }
+ let(:ci_default_branch) { 'master' }
+ let(:qa_eval_report_filename) { 'report.md' }
+ let(:merge_request_iid) { "123" }
+ let(:ci_project_id) { "456" }
+ let(:ci_project_dir) { "/builds/gitlab-org/gitlab" }
+ let(:base_dir) { "#{ci_project_dir}/#{qa_eval_report_filename}" }
+
+ before do
+ stub_env('QA_EVAL_REPORT_FILENAME', qa_eval_report_filename)
+ stub_env('CI_MERGE_REQUEST_IID', merge_request_iid)
+ stub_env('CI_PROJECT_ID', ci_project_id)
+ stub_env('CI_PROJECT_DIR', ci_project_dir)
+ end
+
+ context 'when a note does not already exist' do
+ let(:note) { nil } # rubocop: disable RSpec/VerifiedDoubles -- an internal detail of Gitlab gem.
+
+ it 'saves the report as a markdown file and creates a new MR note containing the report content' do
+ expect(File).to receive(:write).with(base_dir, reporter.markdown_report)
+
+ allow(reporter).to receive(:existing_report_note).and_return(note)
+ expect(client).to receive(:create_merge_request_note).with(
+ ci_project_id,
+ merge_request_iid,
+ reporter.markdown_report
+ )
+
+ reporter.run
+ end
+ end
+
+ context 'when a note exists' do
+ let(:note_id) { "1" }
+ let(:note) { double(id: note_id, type: "Note") } # rubocop: disable RSpec/VerifiedDoubles -- an internal detail of Gitlab gem.
+
+ it 'saves the report as a markdown file and updates the existing MR note containing the report content' do
+ expect(File).to receive(:write).with(base_dir, reporter.markdown_report)
+
+ allow(reporter).to receive(:existing_report_note).and_return(note)
+ expect(client).to receive(:edit_merge_request_note).with(
+ ci_project_id,
+ merge_request_iid,
+ note_id,
+ reporter.markdown_report
+ )
+
+ reporter.run
+ end
+ end
+ end
+ end
+
+ describe '#markdown_report' do
+ let(:mock_data) do
+ [
+ {
+ "question" => "question1",
+ "resource" => "resource",
+ "answer" => "answer1",
+ "tools_used" => ["foobar tool"],
+ "evaluations" => [
+ { "model" => "claude-2", "response" => "Grade: CORRECT" },
+ { "model" => "text-bison", "response" => "Grade: CORRECT" }
+ ]
+ },
+ {
+ "question" => "question2",
+ "resource" => "resource",
+ "answer" => "answer2",
+ "tools_used" => [],
+ "evaluations" => [
+ { "model" => "claude-2", "response" => " Grade: INCORRECT" },
+ { "model" => "text-bison", "response" => "Grade: INCORRECT" }
+ ]
+ },
+ {
+ "question" => "question3",
+ "resource" => "resource",
+ "answer" => "answer3",
+ "tools_used" => [],
+ "evaluations" => [
+ { "model" => "claude-2", "response" => " Grade: CORRECT" },
+ { "model" => "text-bison", "response" => "Grade: INCORRECT" }
+ ]
+ },
+ {
+ "question" => "question4",
+ "resource" => "resource",
+ "answer" => "answer4",
+ "tools_used" => [],
+ # Note: The first evaluation (claude-2) is considered invalid and ignored.
+ "evaluations" => [
+ { "model" => "claude-2", "response" => "???" },
+ { "model" => "text-bison", "response" => "Grade: CORRECT" }
+ ]
+ },
+ {
+ "question" => "question5",
+ "resource" => "resource",
+ "answer" => "answer5",
+ "tools_used" => [],
+ # Note: The second evaluation (text-bison) is considered invalid and ignored.
+ "evaluations" => [
+ { "model" => "claude-2", "response" => " Grade: INCORRECT" },
+ { "model" => "text-bison", "response" => "???" }
+ ]
+ },
+ {
+ "question" => "question6",
+ "resource" => "resource",
+ "answer" => "answer6",
+ "tools_used" => [],
+ # Note: Both evaluations are invalid as they contain neither `CORRECT` nor `INCORRECT`.
+ # It should be ignored in the report.
+ "evaluations" => [
+ { "model" => "claude-2", "response" => "???" },
+ { "model" => "text-bison", "response" => "???" }
+ ]
+ }
+ ]
+ end
+
+ before do
+ allow(reporter).to receive(:report_data).and_return(mock_data)
+ end
+
+ it "generates the correct summary stats and uses the correct emoji indicators" do
+ expect(reporter.markdown_report).to include "The total number of evaluations: 5"
+
+ expect(reporter.markdown_report).to include "all LLMs graded `CORRECT`: 2 (40.0%)"
+ expect(reporter.markdown_report).to include ":white_check_mark: :white_check_mark:"
+ expect(reporter.markdown_report).to include ":warning: :white_check_mark:"
+
+ expect(reporter.markdown_report).to include "all LLMs graded `INCORRECT`: 2 (40.0%)"
+ expect(reporter.markdown_report).to include ":x: :x:"
+ expect(reporter.markdown_report).to include ":x: :warning:"
+
+ expect(reporter.markdown_report).to include "in which LLMs disagreed: 1 (20.0%)"
+ expect(reporter.markdown_report).to include ":white_check_mark: :x:"
+ end
+
+ it "includes the tools used" do
+ expect(reporter.markdown_report).to include "[\"foobar tool\"]"
+ end
+
+ context 'when usernames are present' do
+ let(:mock_data) do
+ [
+ {
+ "question" => "@user's @root?",
+ "resource" => "resource",
+ "answer" => "@user2 and @user3",
+ "tools_used" => ["foobar tool"],
+ "evaluations" => [
+ { "model" => "claude-2", "response" => "Grade: CORRECT\n\n@user4" },
+ { "model" => "text-bison", "response" => "Grade: CORRECT\n\n@user5" }
+ ]
+ }
+ ]
+ end
+
+ it 'quotes the usernames with backticks' do
+ expect(reporter.markdown_report).to include "`@root`"
+ expect(reporter.markdown_report).to include "`@user`"
+ expect(reporter.markdown_report).to include "`@user2`"
+ expect(reporter.markdown_report).to include "`@user3`"
+ expect(reporter.markdown_report).to include "`@user4`"
+ expect(reporter.markdown_report).to include "`@user5`"
+ end
+ end
+ end
+end
diff --git a/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb b/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb
index aa758e19dfa..9b191215739 100644
--- a/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb
+++ b/spec/scripts/generate_message_to_run_e2e_pipeline_spec.rb
@@ -224,8 +224,8 @@ RSpec.describe GenerateMessageToRunE2ePipeline, feature_category: :tooling do
<!-- Run e2e warning begin -->
@#{author_username} Some end-to-end (E2E) tests should run based on the stage label.
- Please start the `trigger-omnibus-and-follow-up-e2e` job in the `qa` stage and ensure tests in the `follow-up-e2e:package-and-test-ee` pipeline
- pass **before this MR is merged**.
+ Please start the `trigger-omnibus-and-follow-up-e2e` job in the `qa` stage and wait for the tests in the `follow-up-e2e:package-and-test-ee` pipeline
+ to pass **before merging this MR**. Do not use **Auto-merge**, unless these tests have already completed successfully, because a failure in these tests do not block the auto-merge.
(E2E tests are computationally intensive and don't run automatically for every push/rebase, so we ask you to run this job manually at least once.)
To run all E2E tests, apply the ~"pipeline:run-all-e2e" label and run a new pipeline.
@@ -235,7 +235,7 @@ RSpec.describe GenerateMessageToRunE2ePipeline, feature_category: :tooling do
Once done, apply the ✅ emoji on this comment.
- **Team members only:** for any questions or help, reach out on the internal `#quality` Slack channel.
+ **Team members only:** for any questions or help, reach out on the internal `#test-platform` Slack channel.
<!-- Run e2e warning end -->
MARKDOWN
end
diff --git a/spec/scripts/internal_events/cli_spec.rb b/spec/scripts/internal_events/cli_spec.rb
new file mode 100644
index 00000000000..d84a4498fe8
--- /dev/null
+++ b/spec/scripts/internal_events/cli_spec.rb
@@ -0,0 +1,866 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'tty/prompt/test'
+require_relative '../../../scripts/internal_events/cli'
+
+RSpec.describe Cli, feature_category: :service_ping do
+ let(:prompt) { TTY::Prompt::Test.new }
+ let(:files_to_cleanup) { [] }
+
+ let(:event1_filepath) { 'config/events/internal_events_cli_used.yml' }
+ let(:event1_content) { internal_event_fixture('events/event_with_identifiers.yml') }
+ let(:event2_filepath) { 'ee/config/events/internal_events_cli_opened.yml' }
+ let(:event2_content) { internal_event_fixture('events/ee_event_without_identifiers.yml') }
+ let(:event3_filepath) { 'config/events/internal_events_cli_closed.yml' }
+ let(:event3_content) { internal_event_fixture('events/secondary_event_with_identifiers.yml') }
+
+ before do
+ stub_milestone('16.6')
+ collect_file_writes(files_to_cleanup)
+ stub_product_groups(File.read('spec/fixtures/scripts/internal_events/stages.yml'))
+ stub_helper(:fetch_window_size, '50')
+ end
+
+ after do
+ delete_files(files_to_cleanup)
+ end
+
+ shared_examples 'creates the right defintion files' do |description, test_case = {}|
+ # For expected keystroke mapping, see https://github.com/piotrmurach/tty-reader/blob/master/lib/tty/reader/keys.rb
+ let(:keystrokes) { test_case.dig('inputs', 'keystrokes') || [] }
+ let(:input_files) { test_case.dig('inputs', 'files') || [] }
+ let(:output_files) { test_case.dig('outputs', 'files') || [] }
+
+ subject { run_with_verbose_timeout }
+
+ it "in scenario: #{description}" do
+ delete_old_ouputs # just in case
+ prep_input_files
+ queue_cli_inputs(keystrokes)
+ expect_file_creation
+
+ subject
+ end
+
+ private
+
+ def delete_old_ouputs
+ [input_files, output_files].flatten.each do |file_info|
+ FileUtils.rm_f(Rails.root.join(file_info['path']))
+ end
+ end
+
+ def prep_input_files
+ input_files.each do |file|
+ File.write(
+ Rails.root.join(file['path']),
+ File.read(Rails.root.join(file['content']))
+ )
+ end
+ end
+
+ def expect_file_creation
+ if output_files.any?
+ output_files.each do |file|
+ expect(File).to receive(:write).with(file['path'], File.read(file['content']))
+ end
+ else
+ expect(File).not_to receive(:write)
+ end
+ end
+ end
+
+ context 'when creating new events' do
+ YAML.safe_load(File.read('spec/fixtures/scripts/internal_events/new_events.yml')).each do |test_case|
+ it_behaves_like 'creates the right defintion files', test_case['description'], test_case
+ end
+ end
+
+ context 'when creating new metrics' do
+ YAML.safe_load(File.read('spec/fixtures/scripts/internal_events/new_metrics.yml')).each do |test_case|
+ it_behaves_like 'creates the right defintion files', test_case['description'], test_case
+ end
+
+ context 'when creating a metric from multiple events' do
+ let(:events) do
+ [{
+ action: '00_event1', category: 'InternalEventTracking',
+ product_section: 'dev', product_stage: 'plan', product_group: 'optimize'
+ }, {
+ action: '00_event2', category: 'InternalEventTracking',
+ product_section: 'dev', product_stage: 'create', product_group: 'ide'
+ }, {
+ action: '00_event3', category: 'InternalEventTracking',
+ product_section: 'dev', product_stage: 'create', product_group: 'source_code'
+ }]
+ end
+
+ before do
+ events.each do |event|
+ File.write("config/events/#{event[:action]}.yml", event.transform_keys(&:to_s).to_yaml)
+ end
+ end
+
+ it 'filters the product group options based on common section' do
+ # Select 00_event1 & #00_event2
+ queue_cli_inputs([
+ "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ "2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+ " ", # Multi-select: __event1
+ "\e[B", # Arrow down to: __event2
+ " ", # Multi-select: __event2
+ "\n", # Submit selections
+ "\n", # Select: Weekly/Monthly count of unique users
+ "aggregate metric description\n", # Submit description
+ "\n", # Accept description for weekly
+ "\n" # Copy & continue
+ ])
+
+ run_with_timeout
+
+ # Filter down to "dev" options
+ expect(plain_last_lines(9)).to eq <<~TEXT.chomp
+ ‣ dev:plan:project_management
+ dev:plan:product_planning
+ dev:plan:knowledge
+ dev:plan:optimize
+ dev:create:source_code
+ dev:create:code_review
+ dev:create:ide
+ dev:create:editor_extensions
+ dev:create:code_creation
+ TEXT
+ end
+
+ it 'filters the product group options based on common section & stage' do
+ # Select 00_event2 & #00_event3
+ queue_cli_inputs([
+ "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ "2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+ "\e[B", # Arrow down to: __event2
+ " ", # Multi-select: __event2
+ "\e[B", # Arrow down to: __event3
+ " ", # Multi-select: __event3
+ "\n", # Submit selections
+ "\n", # Select: Weekly/Monthly count of unique users
+ "aggregate metric description\n", # Submit description
+ "\n", # Accept description for weekly
+ "\n" # Copy & continue
+ ])
+
+ run_with_timeout
+
+ # Filter down to "dev:create" options
+ expect(plain_last_lines(5)).to eq <<~TEXT.chomp
+ ‣ dev:create:source_code
+ dev:create:code_review
+ dev:create:ide
+ dev:create:editor_extensions
+ dev:create:code_creation
+ TEXT
+ end
+ end
+
+ context 'when product group for event no longer exists' do
+ let(:event) do
+ {
+ action: '00_event1', category: 'InternalEventTracking',
+ product_section: 'other', product_stage: 'other', product_group: 'other'
+ }
+ end
+
+ before do
+ File.write("config/events/#{event[:action]}.yml", event.transform_keys(&:to_s).to_yaml)
+ end
+
+ it 'prompts user to select another group' do
+ queue_cli_inputs([
+ "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
+ "\n", # Select: 00__event1
+ "\n", # Select: Weekly/Monthly count of unique users
+ "aggregate metric description\n", # Submit description
+ "\n", # Accept description for weekly
+ "2\n" # Modify attributes
+ ])
+
+ run_with_timeout
+
+ # Filter down to "dev" options
+ expect(plain_last_lines(50)).to include 'Select one: Which group owns the metric?'
+ end
+ end
+
+ context 'when creating a metric for an event which has metrics' do
+ before do
+ File.write(event1_filepath, File.read(event1_content))
+ end
+
+ it 'shows all metrics options' do
+ select_event_from_list
+
+ expect(plain_last_lines(5)).to eq <<~TEXT.chomp
+ ‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
+ Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
+ Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
+ Monthly/Weekly count of [internal_events_cli_used occurrences]
+ Total count of [internal_events_cli_used occurrences]
+ TEXT
+ end
+
+ context 'with an existing weekly metric' do
+ before do
+ File.write(
+ 'ee/config/metrics/counts_7d/count_total_internal_events_cli_used_weekly.yml',
+ File.read('spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml')
+ )
+ end
+
+ it 'partially filters metric options' do
+ select_event_from_list
+
+ expect(plain_last_lines(6)).to eq <<~TEXT.chomp
+ ‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
+ Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
+ Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
+ Monthly count of [internal_events_cli_used occurrences]
+ ✘ Weekly count of [internal_events_cli_used occurrences] (already defined)
+ Total count of [internal_events_cli_used occurrences]
+ TEXT
+ end
+ end
+
+ context 'with an existing total metric' do
+ before do
+ File.write(
+ 'ee/config/metrics/counts_all/count_total_internal_events_cli_used.yml',
+ File.read('spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml')
+ )
+ end
+
+ it 'filters whole metric options' do
+ select_event_from_list
+
+ expect(plain_last_lines(5)).to eq <<~TEXT.chomp
+ ‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
+ Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
+ Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
+ Monthly/Weekly count of [internal_events_cli_used occurrences]
+ ✘ Total count of [internal_events_cli_used occurrences] (already defined)
+ TEXT
+ end
+ end
+
+ private
+
+ def select_event_from_list
+ queue_cli_inputs([
+ "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
+ 'internal_events_cli_used', # Filters to this event
+ "\n" # Select: config/events/internal_events_cli_used.yml
+ ])
+
+ run_with_timeout
+ end
+ end
+
+ context 'when event excludes identifiers' do
+ before do
+ File.write(event2_filepath, File.read(event2_content))
+ end
+
+ it 'filters unavailable identifiers' do
+ queue_cli_inputs([
+ "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
+ 'internal_events_cli_opened', # Filters to this event
+ "\n" # Select: config/events/internal_events_cli_opened.yml
+ ])
+
+ run_with_timeout
+
+ expect(plain_last_lines(5)).to eq <<~TEXT.chomp
+ ✘ Monthly/Weekly count of unique users [who triggered internal_events_cli_opened] (user unavailable)
+ ✘ Monthly/Weekly count of unique projects [where internal_events_cli_opened occurred] (project unavailable)
+ ✘ Monthly/Weekly count of unique namespaces [where internal_events_cli_opened occurred] (namespace unavailable)
+ ‣ Monthly/Weekly count of [internal_events_cli_opened occurrences]
+ Total count of [internal_events_cli_opened occurrences]
+ TEXT
+ end
+ end
+
+ context 'when all metrics already exist' do
+ let(:event) { { action: '00_event1', category: 'InternalEventTracking' } }
+ let(:metric) { { options: { 'events' => ['00_event1'] }, events: [{ 'name' => '00_event1' }] } }
+
+ let(:files) do
+ [
+ ['config/events/00_event1.yml', event],
+ ['config/metrics/counts_all/count_total_00_event1.yml', metric.merge(time_frame: 'all')],
+ ['config/metrics/counts_7d/count_total_00_event1_weekly.yml', metric.merge(time_frame: '7d')],
+ ['config/metrics/counts_28d/count_total_00_event1_monthly.yml', metric.merge(time_frame: '28d')]
+ ]
+ end
+
+ before do
+ files.each do |path, content|
+ File.write(path, content.transform_keys(&:to_s).to_yaml)
+ end
+ end
+
+ it 'exits the script and directs user to search for existing metrics' do
+ queue_cli_inputs([
+ "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ "1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
+ '00_event1', # Filters to this event
+ "\n" # Select: config/events/00_event1.yml
+ ])
+
+ run_with_timeout
+
+ expect(plain_last_lines(15)).to include 'Looks like the potential metrics for this event ' \
+ 'either already exist or are unsupported.'
+ end
+ end
+ end
+
+ context 'when showing usage examples' do
+ let(:expected_example_prompt) do
+ <<~TEXT.chomp
+ Select one: Select a use-case to view examples for: (Press ↑/↓ arrow or 1-8 number to move and Enter to select)
+ ‣ 1. ruby/rails
+ 2. rspec
+ 3. javascript (vue)
+ 4. javascript (plain)
+ 5. vue template
+ 6. haml
+ 7. View examples for a different event
+ 8. Exit
+ TEXT
+ end
+
+ context 'for an event with identifiers' do
+ let(:expected_rails_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ # RAILS
+
+ Gitlab::InternalEvents.track_event(
+ 'internal_events_cli_used',
+ project: project,
+ namespace: project.namespace,
+ user: user
+ )
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ let(:expected_rspec_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ # RSPEC
+
+ it_behaves_like 'internal event tracking' do
+ let(:event) { 'internal_events_cli_used' }
+ let(:project) { project }
+ let(:namespace) { project.namespace }
+ let(:user) { user }
+ end
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ before do
+ File.write(event1_filepath, File.read(event1_content))
+ end
+
+ it 'shows backend examples' do
+ queue_cli_inputs([
+ "3\n", # Enum-select: View Usage -- look at code examples for an existing event
+ 'internal_events_cli_used', # Filters to this event
+ "\n", # Select: config/events/internal_events_cli_used.yml
+ "\n", # Select: ruby/rails
+ "\e[B", # Arrow down to: rspec
+ "\n", # Select: rspec
+ "8\n" # Exit
+ ])
+
+ run_with_timeout
+
+ output = plain_last_lines(100)
+
+ expect(output).to include expected_example_prompt
+ expect(output).to include expected_rails_example
+ expect(output).to include expected_rspec_example
+ end
+ end
+
+ context 'for an event without identifiers' do
+ let(:expected_rails_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ # RAILS
+
+ Gitlab::InternalEvents.track_event('internal_events_cli_opened')
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ let(:expected_rspec_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ # RSPEC
+
+ it_behaves_like 'internal event tracking' do
+ let(:event) { 'internal_events_cli_opened' }
+ end
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ let(:expected_vue_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ // VUE
+
+ <script>
+ import { InternalEvents } from '~/tracking';
+ import { GlButton } from '@gitlab/ui';
+
+ const trackingMixin = InternalEvents.mixin();
+
+ export default {
+ mixins: [trackingMixin],
+ components: { GlButton },
+ methods: {
+ performAction() {
+ this.trackEvent('internal_events_cli_opened');
+ },
+ },
+ };
+ </script>
+
+ <template>
+ <gl-button @click=performAction>Click Me</gl-button>
+ </template>
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ let(:expected_js_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ // FRONTEND -- RAW JAVASCRIPT
+
+ import { InternalEvents } from '~/tracking';
+
+ export const performAction = () => {
+ InternalEvents.trackEvent('internal_events_cli_opened');
+
+ return true;
+ };
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ let(:expected_vue_template_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ // VUE TEMPLATE -- ON-CLICK
+
+ <script>
+ import { GlButton } from '@gitlab/ui';
+
+ export default {
+ components: { GlButton }
+ };
+ </script>
+
+ <template>
+ <gl-button data-event-tracking="internal_events_cli_opened">
+ Click Me
+ </gl-button>
+ </template>
+
+ --------------------------------------------------
+ // VUE TEMPLATE -- ON-LOAD
+
+ <script>
+ import { GlButton } from '@gitlab/ui';
+
+ export default {
+ components: { GlButton }
+ };
+ </script>
+
+ <template>
+ <gl-button data-event-tracking-load="internal_events_cli_opened">
+ Click Me
+ </gl-button>
+ </template>
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ let(:expected_haml_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ # HAML -- ON-CLICK
+
+ .gl-display-inline-block{ data: { event_tracking: 'internal_events_cli_opened' } }
+ = _('Important Text')
+
+ --------------------------------------------------
+ # HAML -- COMPONENT ON-CLICK
+
+ = render Pajamas::ButtonComponent.new(button_options: { data: { event_tracking: 'internal_events_cli_opened' } })
+
+ --------------------------------------------------
+ # HAML -- COMPONENT ON-LOAD
+
+ = render Pajamas::ButtonComponent.new(button_options: { data: { event_tracking_load: true, event_tracking: 'internal_events_cli_opened' } })
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ before do
+ File.write(event2_filepath, File.read(event2_content))
+ end
+
+ it 'shows all examples' do
+ queue_cli_inputs([
+ "3\n", # Enum-select: View Usage -- look at code examples for an existing event
+ 'internal_events_cli_opened', # Filters to this event
+ "\n", # Select: config/events/internal_events_cli_used.yml
+ "\n", # Select: ruby/rails
+ "\e[B", # Arrow down to: rspec
+ "\n", # Select: rspec
+ "\e[B", # Arrow down to: js vue
+ "\n", # Select: js vue
+ "\e[B", # Arrow down to: js plain
+ "\n", # Select: js plain
+ "\e[B", # Arrow down to: vue template
+ "\n", # Select: vue template
+ "\e[B", # Arrow down to: haml
+ "\n", # Select: haml
+ "8\n" # Exit
+ ])
+
+ run_with_timeout
+
+ output = plain_last_lines(1000)
+
+ expect(output).to include expected_example_prompt
+ expect(output).to include expected_rails_example
+ expect(output).to include expected_rspec_example
+ expect(output).to include expected_vue_example
+ expect(output).to include expected_js_example
+ expect(output).to include expected_vue_template_example
+ expect(output).to include expected_haml_example
+ end
+ end
+
+ context 'when viewing examples for multiple events' do
+ let(:expected_event1_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ # RAILS
+
+ Gitlab::InternalEvents.track_event(
+ 'internal_events_cli_used',
+ project: project,
+ namespace: project.namespace,
+ user: user
+ )
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ let(:expected_event2_example) do
+ <<~TEXT.chomp
+ --------------------------------------------------
+ # RAILS
+
+ Gitlab::InternalEvents.track_event('internal_events_cli_opened')
+
+ --------------------------------------------------
+ TEXT
+ end
+
+ before do
+ File.write(event1_filepath, File.read(event1_content))
+ File.write(event2_filepath, File.read(event2_content))
+ end
+
+ it 'switches between events gracefully' do
+ queue_cli_inputs([
+ "3\n", # Enum-select: View Usage -- look at code examples for an existing event
+ 'internal_events_cli_used', # Filters to this event
+ "\n", # Select: config/events/internal_events_cli_used.yml
+ "\n", # Select: ruby/rails
+ "7\n", # Select: View examples for a different event
+ 'internal_events_cli_opened', # Filters to this event
+ "\n", # Select: config/events/internal_events_cli_opened.yml
+ "\n", # Select: ruby/rails
+ "8\n" # Exit
+ ])
+
+ run_with_timeout
+
+ output = plain_last_lines(300)
+
+ expect(output).to include expected_example_prompt
+ expect(output).to include expected_event1_example
+ expect(output).to include expected_event2_example
+ end
+ end
+ end
+
+ context 'when offline' do
+ before do
+ stub_product_groups(nil)
+ end
+
+ it_behaves_like 'creates the right defintion files',
+ 'Creates a new event with product stage/section/group input manually' do
+ let(:keystrokes) do
+ [
+ "1\n", # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+ "Internal Event CLI is opened\n", # Submit description
+ "internal_events_cli_opened\n", # Submit action name
+ "6\n", # Select: None
+ "\n", # Skip MR URL
+ "analytics\n", # Input section
+ "monitor\n", # Input stage
+ "analytics_instrumentation\n", # Input group
+ "2\n", # Select [premium, ultimate]
+ "y\n", # Create file
+ "3\n" # Exit
+ ]
+ end
+
+ let(:output_files) { [{ 'path' => event2_filepath, 'content' => event2_content }] }
+ end
+
+ it_behaves_like 'creates the right defintion files',
+ 'Creates a new metric with product stage/section/group input manually' do
+ let(:keystrokes) do
+ [
+ "2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ "2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions
+ 'internal_events_cli', # Filters to the relevant events
+ ' ', # Multi-select: internal_events_cli_closed
+ "\e[B", # Arrow down to: internal_events_cli_used
+ ' ', # Multi-select: internal_events_cli_used
+ "\n", # Submit selections
+ "\e[B", # Arrow down to: Weekly count of unique projects
+ "\n", # Select: Weekly count of unique projects
+ "where a defition file was created with the CLI\n", # Input description
+ "\n", # Submit weekly description for monthly
+ "2\n", # Select: Modify attributes
+ "\n", # Accept section
+ "\n", # Accept stage
+ "\n", # Accept group
+ "\n", # Skip URL
+ "1\n", # Select: [free, premium, ultimate]
+ "y\n", # Create file
+ "y\n", # Create file
+ "2\n" # Exit
+ ]
+ end
+
+ let(:input_files) do
+ [
+ { 'path' => event1_filepath, 'content' => event1_content },
+ { 'path' => event3_filepath, 'content' => event3_content }
+ ]
+ end
+
+ let(:output_files) do
+ # rubocop:disable Layout/LineLength -- Long filepaths read better unbroken
+ [{
+ 'path' => 'config/metrics/counts_28d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly.yml',
+ 'content' => 'spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml'
+ }, {
+ 'path' => 'config/metrics/counts_7d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly.yml',
+ 'content' => 'spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml'
+ }]
+ # rubocop:enable Layout/LineLength
+ end
+ end
+ end
+
+ context 'when window size is unavailable' do
+ before do
+ # `tput <cmd>` returns empty string on error
+ stub_helper(:fetch_window_size, '')
+ stub_helper(:fetch_window_height, '')
+ end
+
+ it_behaves_like 'creates the right defintion files',
+ 'Terminal size does not prevent file creation' do
+ let(:keystrokes) do
+ [
+ "1\n", # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
+ "Internal Event CLI is opened\n", # Submit description
+ "internal_events_cli_opened\n", # Submit action name
+ "6\n", # Select: None
+ "\n", # Skip MR URL
+ "instrumentation\n", # Filter & select group
+ "2\n", # Select [premium, ultimate]
+ "y\n", # Create file
+ "3\n" # Exit
+ ]
+ end
+
+ let(:output_files) { [{ 'path' => event2_filepath, 'content' => event2_content }] }
+ end
+ end
+
+ context "when user doesn't know what they're trying to do" do
+ it "handles when user isn't trying to track product usage" do
+ queue_cli_inputs([
+ "4\n", # Enum-select: ...am I in the right place?
+ "n\n" # No --> Are you trying to track customer usage of a GitLab feature?
+ ])
+
+ run_with_timeout
+
+ expect(plain_last_lines(50)).to include("Oh no! This probably isn't the tool you need!")
+ end
+
+ it "handles when product usage can't be tracked with events" do
+ queue_cli_inputs([
+ "4\n", # Enum-select: ...am I in the right place?
+ "y\n", # Yes --> Are you trying to track customer usage of a GitLab feature?
+ "n\n" # No --> Can usage for the feature be measured by tracking a specific user action?
+ ])
+
+ run_with_timeout
+
+ expect(plain_last_lines(50)).to include("Oh no! This probably isn't the tool you need!")
+ end
+
+ it 'handles when user needs to add a new event' do
+ queue_cli_inputs([
+ "4\n", # Enum-select: ...am I in the right place?
+ "y\n", # Yes --> Are you trying to track customer usage of a GitLab feature?
+ "y\n", # Yes --> Can usage for the feature be measured by tracking a specific user action?
+ "n\n", # No --> Is the event already tracked?
+ "n\n" # No --> Ready to start?
+ ])
+
+ run_with_timeout
+
+ expect(plain_last_lines(30)).to include("Okay! The next step is adding a new event! (~5 min)")
+ end
+
+ it 'handles when user needs to add a new metric' do
+ queue_cli_inputs([
+ "4\n", # Enum-select: ...am I in the right place?
+ "y\n", # Yes --> Are you trying to track customer usage of a GitLab feature?
+ "y\n", # Yes --> Can usage for the feature be measured by tracking a specific user action?
+ "y\n", # Yes --> Is the event already tracked?
+ "n\n" # No --> Ready to start?
+ ])
+
+ run_with_timeout
+
+ expect(plain_last_lines(30)).to include("Amazing! The next step is adding a new metric! (~8 min)")
+ end
+ end
+
+ private
+
+ def queue_cli_inputs(keystrokes)
+ prompt.input << keystrokes.join('')
+ prompt.input.rewind
+ end
+
+ def run_with_timeout(duration = 1)
+ Timeout.timeout(duration) { described_class.new(prompt).run }
+ rescue Timeout::Error
+ # Timeout is needed to break out of the CLI, but we may want
+ # to make assertions afterwards
+ end
+
+ def run_with_verbose_timeout(duration = 1)
+ Timeout.timeout(duration) { described_class.new(prompt).run }
+ rescue Timeout::Error => e
+ # Re-raise error so CLI output is printed with the error
+ message = <<~TEXT
+ Awaiting input too long. Entire CLI output:
+
+ #{
+ prompt.output.string.lines
+ .map { |line| "\e[0;37m#{line}\e[0m" } # wrap in white
+ .join('')
+ .gsub("\e[1G", "\e[1G ") # align to error indent
+ }
+
+
+ TEXT
+
+ raise e.class, message, e.backtrace
+ end
+
+ def plain_last_lines(size)
+ prompt.output.string
+ .lines
+ .last(size)
+ .join('')
+ .gsub(/\e[^\sm]{2,4}[mh]/, '')
+ end
+
+ def collect_file_writes(collector)
+ allow(File).to receive(:write).and_wrap_original do |original_method, *args, &block|
+ filepath = args.first
+ collector << filepath
+
+ dirname = Pathname.new(filepath).dirname
+ unless dirname.directory?
+ FileUtils.mkdir_p dirname
+ collector << dirname.to_s
+ end
+
+ original_method.call(*args, &block)
+ end
+ end
+
+ def stub_milestone(milestone)
+ stub_const("InternalEventsCli::Helpers::MILESTONE", milestone)
+ end
+
+ def stub_product_groups(body)
+ allow(Net::HTTP).to receive(:get)
+ .with(URI('https://gitlab.com/gitlab-com/www-gitlab-com/-/raw/master/data/stages.yml'))
+ .and_return(body)
+ end
+
+ def stub_helper(helper, value)
+ # rubocop:disable RSpec/AnyInstanceOf -- 'Next' helper not included in fast_spec_helper & next is insufficient
+ allow_any_instance_of(InternalEventsCli::Helpers).to receive(helper).and_return(value)
+ # rubocop:enable RSpec/AnyInstanceOf
+ end
+
+ def delete_files(files)
+ files.each do |filepath|
+ FileUtils.rm_f(Rails.root.join(filepath))
+ end
+ end
+
+ def internal_event_fixture(filepath)
+ Rails.root.join('spec', 'fixtures', 'scripts', 'internal_events', filepath)
+ end
+end
diff --git a/spec/scripts/lib/glfm/update_specification_spec.rb b/spec/scripts/lib/glfm/update_specification_spec.rb
index 500e8685e77..8269256dc06 100644
--- a/spec/scripts/lib/glfm/update_specification_spec.rb
+++ b/spec/scripts/lib/glfm/update_specification_spec.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'gitlab/rspec/next_instance_of'
+
require_relative '../../../../scripts/lib/glfm/update_specification'
-require_relative '../../../support/helpers/next_instance_of'
# IMPORTANT NOTE: See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#update-specificationrb-script
# for details on the implementation and usage of the `update_specification.rb` script being tested.
diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb
index f46adb1a9f1..a1bedd19ed3 100644
--- a/spec/scripts/trigger-build_spec.rb
+++ b/spec/scripts/trigger-build_spec.rb
@@ -236,14 +236,62 @@ RSpec.describe Trigger, feature_category: :tooling do
describe "TRIGGER_BRANCH" do
context 'when CNG_BRANCH is not set' do
- it 'sets TRIGGER_BRANCH to master' do
- stub_env('CI_PROJECT_NAMESPACE', 'gitlab-org')
- expect(subject.variables['TRIGGER_BRANCH']).to eq('master')
+ context 'with gitlab-org' do
+ before do
+ stub_env('CI_PROJECT_NAMESPACE', 'gitlab-org')
+ end
+
+ it 'sets TRIGGER_BRANCH to master if the commit ref is master' do
+ stub_env('CI_COMMIT_REF_NAME', 'master')
+ stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', nil)
+ expect(subject.variables['TRIGGER_BRANCH']).to eq('master')
+ end
+
+ it 'sets the TRIGGER_BRANCH to master if the commit is part of an MR targeting master' do
+ stub_env('CI_COMMIT_REF_NAME', 'feature_branch')
+ stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', 'master')
+ expect(subject.variables['TRIGGER_BRANCH']).to eq('master')
+ end
+
+ it 'sets TRIGGER_BRANCH to stable branch if the commit ref is a stable branch' do
+ stub_env('CI_COMMIT_REF_NAME', '16-6-stable-ee')
+ expect(subject.variables['TRIGGER_BRANCH']).to eq('16-6-stable')
+ end
+
+ it 'sets the TRIGGER_BRANCH to stable branch if the commit is part of an MR targeting stable branch' do
+ stub_env('CI_COMMIT_REF_NAME', 'feature_branch')
+ stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', '16-6-stable-ee')
+ expect(subject.variables['TRIGGER_BRANCH']).to eq('16-6-stable')
+ end
end
- it 'sets TRIGGER_BRANCH to main-jh on JH side' do
- stub_env('CI_PROJECT_NAMESPACE', 'gitlab-cn')
- expect(subject.variables['TRIGGER_BRANCH']).to eq('main-jh')
+ context 'with gitlab-cn' do
+ before do
+ stub_env('CI_PROJECT_NAMESPACE', 'gitlab-cn')
+ end
+
+ it 'sets TRIGGER_BRANCH to main-jh if commit ref is main-jh' do
+ stub_env('CI_COMMIT_REF_NAME', 'main-jh')
+ stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', nil)
+ expect(subject.variables['TRIGGER_BRANCH']).to eq('main-jh')
+ end
+
+ it 'sets the TRIGGER_BRANCH to main-jh if the commit is part of an MR targeting main-jh' do
+ stub_env('CI_COMMIT_REF_NAME', 'feature_branch')
+ stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', 'main-jh')
+ expect(subject.variables['TRIGGER_BRANCH']).to eq('main-jh')
+ end
+
+ it 'sets TRIGGER_BRANCH to 16-6-stable if commit ref is a stable branch' do
+ stub_env('CI_COMMIT_REF_NAME', '16-6-stable-jh')
+ expect(subject.variables['TRIGGER_BRANCH']).to eq('16-6-stable')
+ end
+
+ it 'sets the TRIGGER_BRANCH to 16-6-stable if the commit is part of an MR targeting 16-6-stable-jh' do
+ stub_env('CI_COMMIT_REF_NAME', 'feature_branch')
+ stub_env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME', '16-6-stable-jh')
+ expect(subject.variables['TRIGGER_BRANCH']).to eq('16-6-stable')
+ end
end
end