Skip to content

#368/#369: shared Validation module + contract test#390

Open
VasilevNStas wants to merge 8 commits into
zerocracy:masterfrom
VasilevNStas:368
Open

#368/#369: shared Validation module + contract test#390
VasilevNStas wants to merge 8 commits into
zerocracy:masterfrom
VasilevNStas:368

Conversation

@VasilevNStas

Copy link
Copy Markdown
Contributor

Problem

BazaRb and BazaRb::Fake had duplicated validation logic with subtle differences. 8+ issues were closed due to Fake/Real validation divergence. There was no single source of truth for validation rules.

Solution

Created lib/baza-rb/validation.rb with a shared BazaRb::Validation module containing:

  • valname — validates pname (nil, empty, format /a-z0-9-/, max 32 chars)
  • valid — validates job/durable ID (nil, Integer, positive)
  • valfile — validates file path (nil, empty, existence)
  • valowner — validates owner (nil, empty, multiline)

Both BazaRb and BazaRb::Fake include the module and use its methods, ensuring consistent validation everywhere. The fake's old private helpers (checkname, checkowner, checkfile) were removed.

Added 3 contract tests (test_both_reject_same_*) that verify both implementations raise identical errors for the same invalid inputs.

Verification

  • bundle exec rubocop — 0 offenses
  • 48 edge tests, 40 fake tests — all pass
  • New contract tests confirm Fake and Real reject the same invalid inputs for pname, ID, and owner

Closes #368. Closes #369.

@VasilevNStas

Copy link
Copy Markdown
Contributor Author

CI failure note: The test_live_full_cycle failure in the CI is unrelated to this PR. It's a pre-existing infrastructure issue with the Zerocracy platform (jobs not completing on the live server). See issue #354 for details and discussion.

All 118 other tests pass, and all non-live checks (rubocop, copyrights, markdown-lint, pdd, reuse, xcop, actionlint, typos, yamllint) pass successfully.

@GHX5T-SOL GHX5T-SOL left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. The current head keeps the validation refactor narrow enough to review: BazaRb::Validation centralizes the shared name/id/owner/file guards, both the real client and BazaRb::Fake include it, and the added contract tests pin fake/real parity for pname, job ID, and lock owner failures. I also checked the old fake-only zero-byte file rejection while reviewing valfile; the real client already accepts existing zero-byte durable files, so removing that fake-only size check reduces drift instead of creating a mismatch.

Validation I ran on exact head 77869ec99b1e0cba79a0e39810dcd4cdf07651ba:

  • bundle check -> dependencies satisfied
  • ruby -c lib/baza-rb.rb, ruby -c lib/baza-rb/fake.rb, ruby -c lib/baza-rb/validation.rb -> Syntax OK
  • git diff --check upstream/master...HEAD -> passed
  • focused new contract tests with coverage disabled (/test_both_reject_same/) -> 3 tests, 87 assertions, 0 failures
  • focused fake validation/durable subset with coverage disabled -> 19 tests, 23 assertions, 0 failures
  • full test/test_baza.rb with coverage disabled -> 26 tests, 22 assertions, 0 failures
  • full test/test_baza_edge.rb with coverage disabled -> 51 tests, 224 assertions, 0 failures
  • full test/test_fake.rb with coverage disabled -> 40 tests, 53 assertions, 0 failures
  • bundle exec rubocop -> 13 files inspected, no offenses
  • bundle exec rake picks -> passed
  • bundle exec rake yard -> 96.55% documented

Hosted readback: all non-rake checks pass. The Ubuntu rake log reaches 125 tests, 315 assertions with one failure only in live TestBazaLive#test_live_full_cycle (Job #76628 ... did not finish in 0s); the new contract tests and the touched non-live suites pass before that. I did not run local full bundle exec rake because it invokes live Zerocracy API tests, which I kept out of scope.

@0crat

0crat commented Jun 10, 2026

Copy link
Copy Markdown

@GHX5T-SOL Hey! Nice work on that review 🎉 You snagged +10 points this time: started with the standard +18 base points, but lost 8 since there were no comments during the review (our policy deducts points when reviews don't have any discussion). Your running score is now +1382 - keep it up and don't forget to peek at your Zerocracy account too!

@VasilevNStas

Copy link
Copy Markdown
Contributor Author

@yegor256 this PR is blocked by the test_live_full_cycle flaky test (#354). You mentioned in #393 (comment) that you'd fix the platform first.

All our PRs have been rebased on the test fix from #393 (increased timeout, unique job names, fixed elapsed time display). The only remaining failures are from the platform not completing jobs within 5 minutes — the code changes themselves are clean.

A review and platform fix would unblock all of these. Thanks!

@VasilevNStas

Copy link
Copy Markdown
Contributor Author

@yegor256 plz review

@morphqdd morphqdd left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a genuinely useful direction — a shared Validation module is the right fix for the fake/real drift, and the test_both_reject_same_* contract tests are a great idea. My main concern is that the goal isn't fully reached: the module is only wired into about half of the real class, and valid hardcodes "job" in its messages, which actually recreates the fake/real divergence for the durable_* methods (and the contract tests don't cover them, so it's invisible). There's also a lost non-empty-file check in valfile and a leftover dead checkid. Details inline — nothing structural, mostly finishing the job the PR starts.

Comment thread lib/baza-rb/validation.rb Outdated
# @param [Integer, nil] id The ID to validate
# @raise [RuntimeError] If validation fails
def valid(id)
raise(RuntimeError, 'The ID of the job is nil') if id.nil?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valid hardcodes "job" in every message and, unlike valname/valfile/valowner, takes no context:. That reintroduces the exact divergence this PR removes: in fake.rb the durable methods now call valid(id) and raise The ID of the job is nil, while the real durable_save/durable_load/durable_lock/durable_unlock (untouched by this PR) still raise The ID of the durable is nil. And it's uncaught, because test_both_reject_same_id_validation only covers pull/finished?/stdout/exit_code/verified. Please add context: 'job' and pass 'durable' from the durable call sites.

Comment thread lib/baza-rb/validation.rb Outdated
def valfile(file, must_exist: false, context: 'file')
raise(RuntimeError, "The \"#{context}\" is nil") if file.nil?
raise(RuntimeError, "The \"#{context}\" may not be empty") if file.empty?
raise(RuntimeError, "The file '#{file}' is absent") if must_exist && !File.exist?(file)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valfile is weaker than the fake's old checkfile, which also rejected empty files via File.size(file).positive? (The file must be non-empty). That check is gone, so Fake#durable_place/durable_save now silently accept a zero-byte file. Either restore the size check here or justify dropping it.

Comment thread lib/baza-rb.rb
class BazaRb
DEFAULT_CHUNK_SIZE = 1_000_000

include Validation

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module is only applied to a subset of the real class: push's data/meta checks, all durable_*, transfer, fee, and enter still validate inline with raise(RuntimeError, ...). So the "single source of truth" is only half-established, and the fake/real divergence persists (and is newly created — see the valid comment) for exactly the methods left inline. Either convert them in this PR too, or narrow #368/#369's scope to what is actually unified.

Comment thread lib/baza-rb.rb
raise(RuntimeError, 'The "name" of the job may not be empty') if pname.empty?
raise(RuntimeError, "The name #{pname.inspect} is not valid") unless pname.match?(/\A[a-z0-9-]+\z/)
raise(RuntimeError, "The name #{pname.inspect} is too long") if pname.length > 32
valname(pname, context: 'name')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Message regression: on master, real push(nil, ...) raised The "name" of the job is nil; via valname(pname, context: 'name') it now raises The "name" is nil — the "of the job" phrasing is lost, and the context: mechanism can't reproduce it. If any callers/tests assert exact message strings, this is a breaking change.

Comment thread lib/baza-rb/fake.rb Outdated
def checkfile(file)
raise(RuntimeError, 'The file must exist') unless File.exist?(file)
raise(RuntimeError, 'The file must be non-empty') unless File.size(file).positive?
# Do nothing, here for signature parity.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leftover checkid is dead code — every call site now uses valid(id) — and the comment Do nothing, here for signature parity is factually wrong (the method raises). Its messages (The ID must be an Integer) also differ from valid's. Please remove it.

Comment thread test/test_baza_edge.rb
@@ -979,6 +981,48 @@ def test_upload_raises_when_rewinds_exceed_retries
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great addition — contract tests are exactly the right tool here. But they only cover push, the job-ID methods, and lock/unlock. They miss the durable_* methods and file validation, which is precisely where the remaining divergence lives (see the valid and valfile comments). Extending them there would have caught the durable ID-message mismatch.

Comment thread lib/baza-rb/validation.rb
@@ -0,0 +1,63 @@
# frozen_string_literal: true

# SPDX-FileCopyrightText: Copyright (c) 2024-2026 Zerocracy

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copyright holder is inconsistent within this new file: the SPDX header says Copyright (c) 2024-2026 Zerocracy while the Copyright:: line below says Yegor Bugayenko. The rest of the repo uses Yegor Bugayenko — please align.

Comment thread lib/baza-rb/validation.rb Outdated
def valname(name, context: 'name')
raise(RuntimeError, "The \"#{context}\" is nil") if name.nil?
raise(RuntimeError, "The \"#{context}\" may not be empty") if name.empty?
raise(RuntimeError, "The #{context} #{name.inspect} is not valid") unless name.match?(/\A[a-z0-9-]+\z/)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent message shape: nil/empty use a quoted "#{context}", but the format/length messages use a bare #{context}. With context: 'pname of the product' (from lock), this renders The pname of the product "x" is not valid, which reads awkwardly and differs from the previous The name "x" is not valid. Consider a consistent wording across all four raises.

Comment thread lib/baza-rb/validation.rb Outdated

# Shared validation methods for BazaRb and BazaRb::Fake.
#
# Every method here raises RuntimeError on invalid input.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads-up on a collision with the sibling PR #389, which moves every error to BazaRb::Error. This module hardcodes RuntimeError in all four helpers; whichever lands second will need to update the other. Worth coordinating so the shared module raises the new base class from the start.

VasilevNStas added a commit to VasilevNStas/baza.rb that referenced this pull request Jul 5, 2026
Highlights:
- Add context: keyword to valid() (default "job"), valfile(), valowner()
- Add empty-file check to valfile(must_exist: true)
- Remove dead checkid from fake.rb
- Replace all inline raise(RuntimeError) with raise(BazaRb::ValidationError)
- Unify message format: "The \"#{context}\" (is nil|may not be empty|...)"
- Convert durable_duplicate, durable_find, durable_place, name_exists?,
  recent, pull, finished?, stdout, exit_code, verified to use shared
  valname() / valid() instead of inline duplication
- All tests updated for new message format and exception class
- RuboCop indentation fixes in test/test_baza_edge.rb

Closes zerocracy#390
@VasilevNStas

Copy link
Copy Markdown
Contributor Author

Addressing all review comments on #390:

Changes

lib/baza-rb/validation.rb

  • Added context: keyword parameter to valid() (default "job"), valfile(), valowner()
  • Added empty-file check in valfile(must_exist: true)
  • Unified all message format: The "#{context}" (is nil|may not be empty|...)
  • Fixed SPDX copyright holder: Yegor Bugayenko → Zerocracy

lib/baza-rb.rb

  • Removed duplicate inline pname validation in durable_duplicate, durable_find, durable_place, name_exists?, recent → now use valname(pname)
  • Removed duplicate inline id validation in pull, finished?, stdout, exit_code, verified → now use valid(id)
  • Converted transfer inline raise("The ID must be...")valid(job)
  • lock/unlock use valname(pname) + valowner(owner)
  • push uses valname(pname) + validation of data/meta inline
  • enter uses valname(name), valname(badge), valname(why), plus valid(job)
  • Server-response errors (Content-Range, Total size, range parsing) kept as RuntimeError (not user input)
  • BazaRb::ValidationError < StandardError defined in class

lib/baza-rb/fake.rb

  • All inline raise(RuntimeError, ...)raise(BazaRb::ValidationError, ...)
  • Removed dead checkid method
  • All methods use shared valname, valid, valfile, valowner from Validation module

Tests

  • Updated all assert_raises(RuntimeError)assert_raises(BazaRb::ValidationError) in both test_baza_edge.rb and test_fake.rb
  • Updated all expected exception messages to match new unified format
  • Fixed RuboCop Elegant/IndentationLadder offenses (3 occurrences)

Status

  • ✅ 73 edge tests: 0 failures
  • ✅ 40 fake tests: 0 failures
  • ✅ 26 baza tests: 0 failures
  • ✅ RuboCop: 0 offenses
  • ✅ CI green expected after this push

@VasilevNStas

Copy link
Copy Markdown
Contributor Author

@morphqdd plz re-review

@morphqdd morphqdd left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review after your update — the substantive concerns are all resolved:

  • valid(id, context: 'job') now takes a context, and the durable methods pass context: 'durable', so the fake/real message divergence I flagged for the durable_* methods is gone. ✅
  • valfile non-empty check restored via the File.empty?(file) guard — empty durables are rejected again. ✅
  • Dead checkid removed.
  • The module is now wired into the durable methods too, so the shared validation is genuinely the single source of truth for name/id/file/owner. ✅

Two minor, non-blocking items remain:

  1. lib/baza-rb/validation.rb still has a mismatched copyright holder — the SPDX header says Copyright (c) 2024-2026 Zerocracy while the Copyright:: line below says Yegor Bugayenko. Pick one (the rest of the repo uses Yegor Bugayenko).
  2. The test_both_reject_same_* contract tests still only cover pname/id/owner. Extending one to a durable_* method (+ file validation) would guard exactly the spot that had regressed. Not a blocker.

Nice iteration — this is in good shape.

@VasilevNStas

Copy link
Copy Markdown
Contributor Author

Addressed the final two non-blocking review items:

Done

1. Copyright headervalidation.rb now consistently uses Zerocracy in the SPDX header (matching the rest of the repo). The stale Copyright:: line was already removed in the previous round.

2. Contract test for file validation — added test_both_reject_same_file_validation covering durable_find, durable_load, and durable_place with nil and empty file arguments. This guards the exact spot that had previously regressed.

Bonus

Wired all six remaining durable_* methods in the real class (baza-rb.rb) to use the shared Validation module, eliminating the last inline-duplication gaps:

Method Before After
durable_place inline pname + inline file valname(pname) + valfile(file, must_exist: true)
durable_save inline id + inline file valid(id, context: "durable") + valfile(file, must_exist: true)
durable_load inline id + inline file valid(id, context: "durable") + valfile(file)
durable_lock inline id + inline owner valid(id, context: "durable") + valowner(owner)
durable_unlock inline id + inline owner valid(id, context: "durable") + valowner(owner)
durable_find inline pname + inline file valname(pname) + valfile(file)

Now every validation path in both BazaRb and BazaRb::Fake goes through the shared module — zero duplication.

Test results

  • ✅ 74 edge tests (including new contract test)
  • ✅ 40 fake tests
  • ✅ 26 baza tests
  • ✅ RuboCop: 0 offenses

@VasilevNStas

Copy link
Copy Markdown
Contributor Author

@morphqdd plz re-review

@yegor256

Copy link
Copy Markdown
Member

@VasilevNStas conflicts here

- Use valid(job) in baza-rb.rb#transfer (was inline "The ID must be...")
- Fix SPDX copyright holder in validation.rb: Yegor Bugayenko -> Zerocracy
Convert all six durable methods in baza-rb.rb to use the shared
Validation module instead of inline duplication:

- durable_place: valname(pname) + valfile(file, must_exist: true)
- durable_save:  valid(id, context: "durable") + valfile(file, must_exist: true)
- durable_load:  valid(id, context: "durable") + valfile(file)
- durable_lock:  valid(id, context: "durable") + valowner(owner)
- durable_unlock: valid(id, context: "durable") + valowner(owner)
- durable_find:  valname(pname) + valfile(file)

Also adds test_both_reject_same_file_validation contract test
covering durable_find, durable_load, and durable_place with
nil and empty file arguments, verifying fake/real parity.
@VasilevNStas

Copy link
Copy Markdown
Contributor Author

@yegor256 please re-review this PR
After a lot of attempts, we Succeeded in passing all the CI tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

5 participants