Files
sodalive-ios/work/tests/check-gift-contact-input.sh
T

199 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
temp_dir="$(mktemp -d)"
trap 'rm -rf "$temp_dir"' EXIT
cat > "$temp_dir/source_contract_verifier.rb" <<'RUBY'
def strip_comments(source)
source.gsub(%r{//[^\n]*}, '').gsub(%r{/\*.*?\*/}m, '')
end
def normalized(source)
strip_comments(source).gsub(/\s+/, ' ').gsub(/\s*([(),:])\s*/, '\1').strip
end
class ContractFailure < StandardError; end
def require_contract(condition, message)
raise ContractFailure, message unless condition
end
def verify_form(source, name_binding, phone_binding)
text = normalized(source)
require_contract(text.scan("GiftContactTextField(text:#{name_binding},rule:.name)").length == 1, 'name field contract')
require_contract(text.scan("GiftContactTextField(text:#{phone_binding},rule:.phoneNumber)").length == 1, 'phone field contract')
require_contract(!text.match?(%r{\bTextField\([^)]*#{Regexp.escape(name_binding)}|\bTextField\([^)]*#{Regexp.escape(phone_binding)}}), 'direct contact TextField binding')
true
end
def can_submit_body(source)
text = strip_comments(source)
declaration = text.match(/\bvar\s+canSubmit\s*:\s*Bool\s*\{/)
require_contract(declaration, 'canSubmit declaration')
marker = declaration.begin(0)
start = text.index('{', marker)
depth = 1
index = start + 1
while index < text.length && depth > 0
depth += 1 if text[index] == '{'
depth -= 1 if text[index] == '}'
index += 1
end
require_contract(depth == 0, 'canSubmit body is balanced')
text[start + 1...index - 1]
end
def verify_model(source, name_property, phone_property)
body = normalized(can_submit_body(source))
name_call = "GiftContactInputRule.name.acceptsSubmission(#{name_property})"
phone_call = "GiftContactInputRule.phoneNumber.acceptsSubmission(#{phone_property})"
expression = body.delete_prefix('return ').strip
require_contract(!expression.start_with?('_ ='), 'discarded submission validation')
require_contract(!expression.include?('return'), 'additional return statement')
operands = expression.split('&&').map(&:strip)
require_contract(operands.include?(name_call), 'name submission validation operand')
require_contract(operands.include?(phone_call), 'phone submission validation operand')
true
end
if ARGV.any?
source = File.read(ARGV[0])
if ARGV[1].start_with?('$')
verify_form(source, ARGV[1], ARGV[2])
else
verify_model(source, ARGV[1], ARGV[2])
end
exit
end
valid_form = <<~SWIFT
struct Form {
// fake GiftContactTextField(text: $wrong, rule: .name)
var body: some View {
GiftContactTextField(
text: $name,
rule: .name
)
GiftContactTextField(text: $phoneNumber,
rule: .phoneNumber)
}
}
SWIFT
verify_form(valid_form, '$name', '$phoneNumber')
puts 'PASS gift-contact-input self-probe: multiline form contract'
invalid_form = <<~SWIFT
// GiftContactTextField(text: $name, rule: .name)
// GiftContactTextField(text: $phoneNumber, rule: .phoneNumber)
// TextField("", text: $name)
// TextField("", text: $phoneNumber)
SWIFT
rejected = false
begin
verify_form(invalid_form, '$name', '$phoneNumber')
abort 'FAIL gift-contact-input self-probe: commented/fake form unexpectedly passed'
rescue ContractFailure
rejected = true
end
abort 'FAIL gift-contact-input self-probe: commented/fake form unexpectedly passed' unless rejected
puts 'PASS gift-contact-input self-probe: commented/fake form rejected'
valid_model = <<~SWIFT
var canSubmit
: Bool {
!isSubmitting &&
GiftContactInputRule.name.acceptsSubmission(name) &&
GiftContactInputRule.phoneNumber.acceptsSubmission(phoneNumber) &&
termsAgreed
}
SWIFT
verify_model(valid_model, 'name', 'phoneNumber')
puts 'PASS gift-contact-input self-probe: canSubmit body contract'
explicit_model = <<~SWIFT
var canSubmit : Bool {
return hasNotSubmitted &&
GiftContactInputRule.name.acceptsSubmission(name) &&
GiftContactInputRule.phoneNumber.acceptsSubmission(phoneNumber) &&
privacyAgreed
}
SWIFT
verify_model(explicit_model, 'name', 'phoneNumber')
puts 'PASS gift-contact-input self-probe: explicit-return conjunction'
invalid_model = <<~SWIFT
var canSubmit : Bool {
_ = GiftContactInputRule.name.acceptsSubmission(name)
_ = GiftContactInputRule.phoneNumber.acceptsSubmission(phoneNumber)
return true
}
SWIFT
rejected = false
begin
verify_model(invalid_model, 'name', 'phoneNumber')
abort 'FAIL gift-contact-input self-probe: fake canSubmit unexpectedly passed'
rescue ContractFailure
rejected = true
end
abort 'FAIL gift-contact-input self-probe: fake canSubmit unexpectedly passed' unless rejected
puts 'PASS gift-contact-input self-probe: fake canSubmit rejected'
RUBY
ruby "$temp_dir/source_contract_verifier.rb"
rule="$root/SodaLive/Sources/V2/Gift/Components/GiftContactInputRule.swift"
if [[ -f "$rule" ]]; then
swiftc -parse-as-library "$rule" "$root/work/tests/GiftContactInputCheck.swift" -o "$temp_dir/gift-contact-input"
else
swiftc -parse-as-library "$root/work/tests/GiftContactInputCheck.swift" -o "$temp_dir/gift-contact-input"
fi
"$temp_dir/gift-contact-input"
component="$root/SodaLive/Sources/V2/Gift/Components/GiftContactTextField.swift"
if [[ ! -f "$component" ]]; then
echo "RED gift-contact-input: missing GiftContactTextField.swift" >&2
exit 1
fi
for probe in 'markedTextRange' 'NSString' 'NSRange' '.numberPad'; do
grep -Fq "$probe" "$component" || { echo "RED gift-contact-input: missing component contract $probe" >&2; exit 1; }
done
typography="$root/SodaLive/Sources/Extensions/FontModifier.swift"
ruby - "$component" "$typography" <<'RUBY'
def source(path)
File.read(path).gsub(%r{//[^\n]*|/\*.*?\*/}m, '').gsub(/\s+/, ' ').strip
end
component = source(ARGV[0])
typography = source(ARGV[1])
abort 'RED gift-contact-input: hardcoded Font.preRegular bypasses localized font policy' if component.include?('Font.preRegular')
abort 'RED gift-contact-input: missing shared body3 localized font resolver' unless component.include?('SodaTypography.body3.localizedFontName()')
abort 'RED gift-contact-input: missing localized UIFont with system regular fallback' unless component.match?(/textField\.font\s*=\s*SodaTypography\.body3\.localizedFontName\(\)\s*\.flatMap\s*\{\s*UIFont\(name:\s*\$0,\s*size:\s*SodaTypography\.body3\.size\)\s*\}\s*\?\?\s*UIFont\.systemFont\(ofSize:\s*SodaTypography\.body3\.size,\s*weight:\s*\.regular\)/)
abort 'RED gift-contact-input: SodaTypography must delegate localized font resolution to existing policy' unless typography.match?(/func localizedFontName\(locale:\s*Locale\s*=\s*\.autoupdatingCurrent\)\s*->\s*String\?\s*\{\s*(?:return\s+)?fontName\(for:\s*\.localized,\s*weight:\s*weight,\s*locale:\s*locale\)\s*\}/)
puts 'PASS gift-contact-input: shared localized typography contract'
RUBY
sender="$root/SodaLive/Sources/V2/Gift/Send/Components/GiftSenderFormView.swift"
recipient="$root/SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressFormView.swift"
sender_model="$root/SodaLive/Sources/V2/Gift/Send/GiftSendViewModel.swift"
recipient_model="$root/SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressViewModel.swift"
for file in "$sender" "$recipient" "$sender_model" "$recipient_model"; do
[[ -f "$file" ]] || { echo "RED gift-contact-input: missing integration file $(basename "$file")" >&2; exit 1; }
done
for file in "$sender" "$recipient"; do
if [[ "$file" == "$sender" ]]; then
ruby "$temp_dir/source_contract_verifier.rb" "$file" '$name' '$phoneNumber'
else
ruby "$temp_dir/source_contract_verifier.rb" "$file" '$viewModel.recipientName' '$viewModel.recipientPhoneNumber'
fi
done
for file in "$sender_model" "$recipient_model"; do
if [[ "$file" == "$sender_model" ]]; then
ruby "$temp_dir/source_contract_verifier.rb" "$file" senderName senderPhoneNumber
else
ruby "$temp_dir/source_contract_verifier.rb" "$file" recipientName recipientPhoneNumber
fi
done
echo "PASS gift-contact-input: source contracts"