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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-03 15:09:25 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-03 15:09:25 +0300
commitaeee636c18f82107ec7a489f33c944c65ad5f34e (patch)
tree2c30286279e096c9114e9a41a3ed07a83293c059 /gems/csv_builder
parent3d8459c18b7a20d9142359bb9334b467e774eb36 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'gems/csv_builder')
-rw-r--r--gems/csv_builder/lib/csv_builder.rb2
-rw-r--r--gems/csv_builder/lib/csv_builder/builder.rb4
-rw-r--r--gems/csv_builder/lib/csv_builder/gzip.rb23
-rw-r--r--gems/csv_builder/spec/csv_builder/gzip_spec.rb33
-rw-r--r--gems/csv_builder/spec/csv_builder_spec.rb177
5 files changed, 157 insertions, 82 deletions
diff --git a/gems/csv_builder/lib/csv_builder.rb b/gems/csv_builder/lib/csv_builder.rb
index 1ef38a1d6a4..86b682939dc 100644
--- a/gems/csv_builder/lib/csv_builder.rb
+++ b/gems/csv_builder/lib/csv_builder.rb
@@ -2,11 +2,13 @@
require 'csv'
require 'tempfile'
+require 'zlib'
require_relative "csv_builder/version"
require_relative "csv_builder/builder"
require_relative "csv_builder/single_batch"
require_relative "csv_builder/stream"
+require_relative "csv_builder/gzip"
# Generates CSV when given a collection and a mapping.
#
diff --git a/gems/csv_builder/lib/csv_builder/builder.rb b/gems/csv_builder/lib/csv_builder/builder.rb
index 3baa2155fc9..99b63153ab2 100644
--- a/gems/csv_builder/lib/csv_builder/builder.rb
+++ b/gems/csv_builder/lib/csv_builder/builder.rb
@@ -59,8 +59,10 @@ module CsvBuilder
@collection.each_batch(order_hint: :created_at) do |relation|
relation.preload(@associations_to_preload).order(:id).each(&block)
end
- else
+ elsif @collection.respond_to?(:find_each)
@collection.find_each(&block)
+ else
+ @collection.each(&block)
end
end
diff --git a/gems/csv_builder/lib/csv_builder/gzip.rb b/gems/csv_builder/lib/csv_builder/gzip.rb
new file mode 100644
index 00000000000..60875006a35
--- /dev/null
+++ b/gems/csv_builder/lib/csv_builder/gzip.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module CsvBuilder
+ class Gzip < CsvBuilder::Builder
+ # Writes the CSV file compressed and yields the written tempfile.
+ #
+ # Example:
+ # > CsvBuilder::Gzip.new(Issue, { title: -> (row) { row.title.upcase }, id: :id }).render do |tempfile|
+ # > puts tempfile.path
+ # > puts `zcat #{tempfile.path}`
+ # > end
+ def render
+ Tempfile.open(['csv_builder_gzip', '.csv.gz']) do |tempfile|
+ csv = CSV.new(Zlib::GzipWriter.open(tempfile.path))
+
+ write_csv csv, until_condition: -> {} # truncation must be handled outside of the CsvBuilder
+
+ csv.close
+ yield tempfile
+ end
+ end
+ end
+end
diff --git a/gems/csv_builder/spec/csv_builder/gzip_spec.rb b/gems/csv_builder/spec/csv_builder/gzip_spec.rb
new file mode 100644
index 00000000000..9d24d351247
--- /dev/null
+++ b/gems/csv_builder/spec/csv_builder/gzip_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CsvBuilder::Gzip do
+ let(:event_1) { double(title: 'Added salt', description: 'A teaspoon') }
+ let(:event_2) { double(title: 'Added sugar', description: 'Just a pinch') }
+ let(:items) { [event_1, event_2] }
+
+ subject(:builder) { described_class.new(items, 'Title' => 'title', 'Description' => 'description') }
+
+ describe '#render' do
+ it 'returns yields a tempfile' do
+ written_content = nil
+
+ builder.render do |tempfile|
+ reader = Zlib::GzipReader.new(tempfile)
+ written_content = reader.read.split("\n")
+ end
+
+ expect(written_content).to eq(
+ [
+ "Title,Description",
+ "Added salt,A teaspoon",
+ "Added sugar,Just a pinch"
+ ])
+ end
+
+ it 'requires a block' do
+ expect { builder.render }.to raise_error(LocalJumpError)
+ end
+ end
+end
diff --git a/gems/csv_builder/spec/csv_builder_spec.rb b/gems/csv_builder/spec/csv_builder_spec.rb
index 9391938f59d..9d6283b3985 100644
--- a/gems/csv_builder/spec/csv_builder_spec.rb
+++ b/gems/csv_builder/spec/csv_builder_spec.rb
@@ -2,126 +2,141 @@
RSpec.describe CsvBuilder do
let(:object) { double(question: :answer) }
- let(:fake_relation) { described_class::FakeRelation.new([object]) }
let(:csv_data) { subject.render }
let(:subject) do
described_class.new(
- fake_relation, 'Q & A' => :question, 'Reversed' => ->(o) { o.question.to_s.reverse })
+ enumerable, 'Q & A' => :question, 'Reversed' => ->(o) { o.question.to_s.reverse })
end
- before do
- stub_const("#{described_class}::FakeRelation", Array)
+ shared_examples 'csv builder examples' do
+ let(:items) { [object] }
- described_class::FakeRelation.class_eval do
- def find_each(&block)
- each(&block)
- end
+ it "has a version number" do
+ expect(CsvBuilder::Version::VERSION).not_to be nil
end
- end
- it "has a version number" do
- expect(CsvBuilder::Version::VERSION).not_to be nil
- end
+ it 'generates a csv' do
+ expect(csv_data.scan(/(,|\n)/).join).to include ",\n,"
+ end
- it 'generates a csv' do
- expect(csv_data.scan(/(,|\n)/).join).to include ",\n,"
- end
+ it 'uses a temporary file to reduce memory allocation' do
+ expect(CSV).to receive(:new).with(instance_of(Tempfile)).and_call_original
- it 'uses a temporary file to reduce memory allocation' do
- expect(CSV).to receive(:new).with(instance_of(Tempfile)).and_call_original
+ subject.render
+ end
- subject.render
- end
+ it 'counts the number of rows' do
+ subject.render
- it 'counts the number of rows' do
- subject.render
+ expect(subject.rows_written).to eq 1
+ end
- expect(subject.rows_written).to eq 1
- end
+ describe 'rows_expected' do
+ it 'uses rows_written if CSV rendered successfully' do
+ subject.render
- describe 'rows_expected' do
- it 'uses rows_written if CSV rendered successfully' do
- subject.render
+ expect(enumerable).not_to receive(:count)
+ expect(subject.rows_expected).to eq 1
+ end
- expect(fake_relation).not_to receive(:count)
- expect(subject.rows_expected).to eq 1
+ it 'falls back to calling .count before rendering begins' do
+ expect(subject.rows_expected).to eq 1
+ end
end
- it 'falls back to calling .count before rendering begins' do
- expect(subject.rows_expected).to eq 1
- end
- end
+ describe 'truncation' do
+ let(:big_object) { double(question: 'Long' * 1024) }
+ let(:row_size) { big_object.question.length * 2 }
+ let(:items) { [big_object, big_object, big_object] }
+
+ it 'occurs after given number of bytes' do
+ expect(subject.render(row_size * 2).length).to be_between(row_size * 2, row_size * 3)
+ expect(subject).to be_truncated
+ expect(subject.rows_written).to eq 2
+ end
+
+ it 'is ignored by default' do
+ expect(subject.render.length).to be > row_size * 3
+ expect(subject.rows_written).to eq 3
+ end
- describe 'truncation' do
- let(:big_object) { double(question: 'Long' * 1024) }
- let(:row_size) { big_object.question.length * 2 }
- let(:fake_relation) { described_class::FakeRelation.new([big_object, big_object, big_object]) }
+ it 'causes rows_expected to fall back to .count' do
+ subject.render(0)
- it 'occurs after given number of bytes' do
- expect(subject.render(row_size * 2).length).to be_between(row_size * 2, row_size * 3)
- expect(subject).to be_truncated
- expect(subject.rows_written).to eq 2
+ expect(enumerable).to receive(:count).and_call_original
+ expect(subject.rows_expected).to eq 3
+ end
end
- it 'is ignored by default' do
- expect(subject.render.length).to be > row_size * 3
- expect(subject.rows_written).to eq 3
+ it 'avoids loading all data in a single query' do
+ expect(enumerable).to receive(:find_each)
+
+ subject.render
end
- it 'causes rows_expected to fall back to .count' do
- subject.render(0)
+ it 'uses hash keys as headers' do
+ expect(csv_data).to start_with 'Q & A'
+ end
- expect(fake_relation).to receive(:count).and_call_original
- expect(subject.rows_expected).to eq 3
+ it 'gets data by calling method provided as hash value' do
+ expect(csv_data).to include 'answer'
end
- end
- it 'avoids loading all data in a single query' do
- expect(fake_relation).to receive(:find_each)
+ it 'allows lamdas to look up more complicated data' do
+ expect(csv_data).to include 'rewsna'
+ end
- subject.render
- end
+ describe 'excel sanitization' do
+ let(:dangerous_title) { double(title: "=cmd|' /C calc'!A0 title", description: "*safe_desc") }
+ let(:dangerous_desc) { double(title: "*safe_title", description: "=cmd|' /C calc'!A0 desc") }
+ let(:items) { [dangerous_title, dangerous_desc] }
+ let(:subject) { described_class.new(enumerable, 'Title' => 'title', 'Description' => 'description') }
+ let(:csv_data) { subject.render }
- it 'uses hash keys as headers' do
- expect(csv_data).to start_with 'Q & A'
- end
+ it 'sanitizes dangerous characters at the beginning of a column' do
+ expect(csv_data).to include "'=cmd|' /C calc'!A0 title"
+ expect(csv_data).to include "'=cmd|' /C calc'!A0 desc"
+ end
- it 'gets data by calling method provided as hash value' do
- expect(csv_data).to include 'answer'
- end
+ it 'does not sanitize safe symbols at the beginning of a column' do
+ expect(csv_data).not_to include "'*safe_desc"
+ expect(csv_data).not_to include "'*safe_title"
+ end
- it 'allows lamdas to look up more complicated data' do
- expect(csv_data).to include 'rewsna'
- end
+ context 'when dangerous characters are after a line break' do
+ let(:items) { [double(title: "Safe title", description: "With task list\n-[x] todo 1")] }
- describe 'excel sanitization' do
- let(:dangerous_title) { double(title: "=cmd|' /C calc'!A0 title", description: "*safe_desc") }
- let(:dangerous_desc) { double(title: "*safe_title", description: "=cmd|' /C calc'!A0 desc") }
- let(:fake_relation) { described_class::FakeRelation.new([dangerous_title, dangerous_desc]) }
- let(:subject) { described_class.new(fake_relation, 'Title' => 'title', 'Description' => 'description') }
- let(:csv_data) { subject.render }
+ it 'does not append single quote to description' do
+ builder = described_class.new(enumerable, 'Title' => 'title', 'Description' => 'description')
- it 'sanitizes dangerous characters at the beginning of a column' do
- expect(csv_data).to include "'=cmd|' /C calc'!A0 title"
- expect(csv_data).to include "'=cmd|' /C calc'!A0 desc"
- end
+ csv_data = builder.render
- it 'does not sanitize safe symbols at the beginning of a column' do
- expect(csv_data).not_to include "'*safe_desc"
- expect(csv_data).not_to include "'*safe_title"
+ expect(csv_data).to eq("Title,Description\nSafe title,\"With task list\n-[x] todo 1\"\n")
+ end
+ end
end
+ end
- context 'when dangerous characters are after a line break' do
- it 'does not append single quote to description' do
- fake_object = double(title: "Safe title", description: "With task list\n-[x] todo 1")
- fake_relation = described_class::FakeRelation.new([fake_object])
- builder = described_class.new(fake_relation, 'Title' => 'title', 'Description' => 'description')
+ context 'when ActiveRecord::Relation like object is given' do
+ let(:enumerable) { described_class::FakeRelation.new(items) }
- csv_data = builder.render
+ before do
+ stub_const("#{described_class}::FakeRelation", Array)
- expect(csv_data).to eq("Title,Description\nSafe title,\"With task list\n-[x] todo 1\"\n")
+ described_class::FakeRelation.class_eval do
+ def find_each(&block)
+ each(&block)
+ end
end
end
+
+ it_behaves_like 'csv builder examples'
+ end
+
+ context 'when Enumerable like object is given' do
+ let(:enumerable) { items }
+
+ it_behaves_like 'csv builder examples'
end
end