주의사항

이 가이드의 목적은 기여자들이 GitLab CE와 EE의 개발 중에 만날 수 있는 잠재적인 “주의사항”을 문서화하는 것입니다.

app/assets 디렉터리에서 파일을 읽지 마십시오

Omnibus GitLab은 자산 컴파일 후 app/assets 디렉터리를 삭제하였습니다.

ee/app/assets, vendor/assets 디렉터리도 삭제됩니다.

이는 Omnibus 설치된 GitLab 인스턴스에서 해당 디렉터리의 파일을 읽는 것이 실패함을 의미합니다:

file = Rails.root.join('app/assets/images/logo.svg')

# 이 파일이 존재하지 않으므로, 읽기 시도는 실패합니다:
# Errno::ENOENT: No such file or directory @ rb_sysopen
File.read(file)

시퀀스 생성 속성의 절대값에 대해 단언하지 마십시오

다음 공장을 고려해 보십시오:

FactoryBot.define do
  factory :label do
    sequence(:title) { |n| "label#{n}" }
  end
end

다음 API 스펙을 고려해 보십시오:

require 'spec_helper'

RSpec.describe API::Labels do
  it '첫 번째 라벨을 생성합니다' do
    create(:label)

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('label1')
  end

  it '두 번째 라벨을 생성합니다' do
    create(:label)

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('label1')
  end
end

실행하면, 이 스펙은 우리가 기대하는 것과 다르게 동작합니다:

1) API::API 시퀀스 문제 재현 두 번째 라벨을 생성합니다
   Failure/Error: expect(json_response.first['name']).to eq('label1')

     expected: "label1"
          got: "label2"

     (compared using ==)

이는 FactoryBot 시퀀스가 각 예제에 대해 초기화되지 않기 때문입니다.

시퀀스 생성 값은 공장을 사용할 때 고유성 제약이 있는 속성을 명시적으로 설정하지 않기 위해 존재함을 기억하세요.

해결 방법

시퀀스 생성 속성 값에 대해 단언하려는 경우, 명시적으로 설정해야 합니다.

또한, 설정한 값은 시퀀스 패턴과 일치하지 않아야 합니다.

예를 들어, :label 공장을 사용할 때, create(:label, title: 'foo')는 괜찮지만 create(:label, title: 'label1')는 안 됩니다.

다음은 수정된 API 스펙입니다:

require 'spec_helper'

RSpec.describe API::Labels do
  it '첫 번째 라벨을 생성합니다' do
    create(:label, title: 'foo')

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('foo')
  end

  it '두 번째 라벨을 생성합니다' do
    create(:label, title: 'bar')

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('bar')
  end
end

expect_any_instance_of 또는 allow_any_instance_of를 RSpec에서 사용하지 마십시오

이유

  • 고립되어 있지 않기 때문에 때때로 깨질 수 있습니다.

  • 우리가 스텁하고자 하는 메서드가 전처리 모듈에 정의되어 있을 경우 작동하지 않습니다. 이는 EE에서 매우 일반적입니다. 다음과 같은 오류가 발생할 수 있습니다:

    1.1) Failure/Error: expect_any_instance_of(ApplicationSetting).to receive_messages(messages)
         Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.
    

대안

대신, 다음 중 하나를 사용하세요:

  • expect_next_instance_of
  • allow_next_instance_of
  • expect_next_found_instance_of
  • allow_next_found_instance_of

예를 들어:

# 이렇게 하지 마세요:
expect_any_instance_of(Project).to receive(:add_import_job)

# 이렇게 하지 마세요:
allow_any_instance_of(Project).to receive(:add_import_job)

다음과 같이 작성할 수 있습니다:

# 이렇게 하세요:
expect_next_instance_of(Project) do |project|
  expect(project).to receive(:add_import_job)
end

# 이렇게 하세요:
allow_next_instance_of(Project) do |project|
  allow(project).to receive(:add_import_job)
end

# 이렇게 하세요:
expect_next_found_instance_of(Project) do |project|
  expect(project).to receive(:add_import_job)
end

# 이렇게 하세요:
allow_next_found_instance_of(Project) do |project|
  allow(project).to receive(:add_import_job)
end

Active Record가 모델 클래스에서 객체를 인스턴스화하기 위해 .new 메서드를 호출하지 않기 때문에,

Active Record 쿼리 및 파인더 메서드에서 반환된 객체에 대해 모의 설정을 하기 위해 expect_next_found_instance_of 또는 allow_next_found_instance_of 모의 헬퍼를 사용해야 합니다.

같은 Active Record 모델의 여러 인스턴스에 대해 모의 및 기대를 설정할 수도 있습니다. expect_next_found_(number)_instances_ofallow_next_found_(number)_instances_of 헬퍼를 사용하여 다음과 같이 할 수 있습니다:

expect_next_found_2_instances_of(Project) do |project|
  expect(project).to receive(:add_import_job)
end

allow_next_found_2_instances_of(Project) do |project|
  allow(project).to receive(:add_import_job)
end

인스턴스를 특정 인수로 초기화하고 싶다면, 다음과 같이 전달할 수도 있습니다:

# 이렇게 하세요:
expect_next_instance_of(MergeRequests::RefreshService, project, user) do |refresh_service|
  expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref)
end

이는 다음을 기대합니다:

# 위의 기대:
refresh_service = MergeRequests::RefreshService.new(project, user)
refresh_service.execute(oldrev, newrev, ref)

Exceptionrescue하지 마세요

“Ruby에서 rescue Exception => e가 나쁜 스타일인 이유는 무엇입니까?”를 참조하세요.

이 규칙은 RuboCop에 의해 자동으로 시행됩니다.

뷰에서 인라인 JavaScript를 사용하지 마세요

인라인 :javascript Haml 필터를 사용할 경우 성능 오버헤드가 발생합니다. 인라인 JavaScript를 사용하는 것은 코드를 구조화하는 좋은 방법이 아니며 피해야 합니다.

우리는 이 두 필터를 제거했습니다.

추가 읽기

사전 컴파일이 필요 없는 자산 저장

사용자에게 제공되어야 하는 자산은 app/assets 디렉토리에 저장되며, 이후 사전 컴파일되어 public/ 디렉토리에 배치됩니다.

그러나 app/assets 내의 파일 내용에 애플리케이션 코드에서 접근할 수 없습니다. 이는 공간 절약 조치로 인해 프로덕션 설치에서 해당 폴더를 포함하지 않기 때문입니다.

support_bot = Users::Internal.support_bot

# `app/assets` 폴더에서 파일 접근하기
support_bot.avatar = Rails.root.join('app', 'assets', 'images', 'bot_avatars', 'support_bot.png').open

support_bot.save!

위 코드는 로컬 환경에서는 작동하지만, 프로덕션 설치에서는 app/assets 폴더가 포함되지 않기 때문에 오류가 발생합니다.

솔루션

대안은 lib/assets 폴더입니다. 다음 조건을 충족하는 자산(이미지 등)을 리포지토리에 추가해야 할 경우 사용하세요:

  • 자산이 사용자에게 직접 제공될 필요가 없고 (따라서 미리 컴파일될 필요도 없음)
  • 자산이 애플리케이션 코드에서 접근되어야 함.

간단히 말해:

app/assets는 미리 컴파일되고 최종 사용자에게 제공되어야 하는 모든 자산을 저장하는 데 사용하세요.

lib/assets는 최종 사용자에게 직접 제공될 필요는 없지만 애플리케이션 코드에서 접근해야 하는 모든 자산을 저장하는 데 사용하세요.

참조용 MR: !37671

has_many through: 또는 has_one through: 연관 관계를 재정의하지 마세요

:through 옵션이 있는 연관 관계는 재정의하지 않는 것이 좋습니다. 우리는 실수로 잘못된 객체를 파괴할 수 있기 때문입니다.

이는 destroy() 메소드가 has_many through:has_one through: 연관 관계에 대해 다르게 동작하기 때문입니다.

group.users.destroy(id)

위의 코드 예제는 User 레코드를 파괴하는 것처럼 보이지만, 실제로는 Member 레코드를 파괴하고 있습니다. 이는 users 연관 관계가 Group에서 has_many through: 연관 관계로 정의되어 있기 때문입니다:

class Group < Namespace
  has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source

  has_many :users, through: :group_members
end

그리고 Rails에서는 이러한 연관 관계에서 destroy()를 사용할 때 다음과 같은 동작을 가지게 됩니다:

:through 옵션이 사용되는 경우, 객체 자체가 아닌 조인 레코드가 대신 파괴됩니다.

그래서 UserGroup을 연결하는 조인 레코드인 Member 레코드가 파괴되고 있습니다.

이제 users 연관 관계를 재정의하면 다음과 같이 됩니다:

class Group < Namespace
  has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source

  has_many :users, through: :group_members

  def users
    super.where(admin: false)
  end
end

재정의된 메소드는 이제 destroy()의 위의 동작을 변경하여 다음과 같이 실행할 경우

group.users.destroy(id)

User 레코드가 삭제되며, 이는 데이터 손실로 이어질 수 있습니다.

간단히 말해, has_many through: 또는 has_one through: 연관 관계를 재정의하는 것은 위험할 수 있습니다.

이를 방지하기 위해 !131455에서 자동 검사를 도입하고 있습니다.

자세한 내용은 이슈 424536을 참조하세요.