Fix bug in parsing TEL content. Rework unit tests.

- newVC3_Tel was not assigning the value provided to the constructed
  object.
- Private unit tests were run every time the code was compiled due to
  how the unittest library works. These now only run as part of the unit
  tests with `nimble test`.
This commit is contained in:
Jonathan Bernard 2023-04-16 03:34:14 -05:00
parent 7b71cb2dfe
commit 68554920e5
6 changed files with 212 additions and 196 deletions

View File

@ -180,11 +180,6 @@ proc getColNumber*(vcl: VCardLexer, pos: int): int =
if vcl.lineStart < pos: return pos - vcl.lineStart if vcl.lineStart < pos: return pos - vcl.lineStart
else: return (vcl.buffer.len - vcl.lineStart) + pos else: return (vcl.buffer.len - vcl.lineStart) + pos
## Unit Tests
## ============================================================================
import std/unittest
proc dumpLexerState*(l: VCardLexer): string = proc dumpLexerState*(l: VCardLexer): string =
result = result =
"pos = " & $l.pos & "\p" & "pos = " & $l.pos & "\p" &
@ -195,7 +190,9 @@ proc dumpLexerState*(l: VCardLexer): string =
"bufEnd = " & $l.bufEnd & "\p" & "bufEnd = " & $l.bufEnd & "\p" &
"buffer = " & l.buffer & "\p" "buffer = " & l.buffer & "\p"
suite "vcard/lexer": ## Unit Tests
## ============================================================================
proc runVcardLexerPrivateTests*() =
const longTestString = const longTestString =
"This is my test string. There are many like it but this one is mine." "This is my test string. There are many like it but this one is mine."
@ -212,36 +209,34 @@ suite "vcard/lexer":
return false return false
return true return true
#test "fillBuffer doesn't double the buffer needlessly":
# var l: VCardLexer
proc readExpected(vcl: var VCardLexer, s: string): bool = proc readExpected(vcl: var VCardLexer, s: string): bool =
for i in 0..<s.len: for i in 0..<s.len:
if vcl.read != s[i]: if vcl.read != s[i]:
return false return false
return true return true
test "can open and fill buffer": # "can open and fill buffer":
block:
var l: VCardLexer var l: VCardLexer
l.open(newStringStream("test")) l.open(newStringStream("test"))
check: assert l.bufferIs("test")
l.bufferIs("test") assert not l.isFull
not l.isFull assert l.readExpected("test")
l.readExpected("test")
test "refills buffer when emptied": # "refills buffer when emptied":
block:
var l: VCardLexer var l: VCardLexer
l.open(newStringStream("test"), 3) l.open(newStringStream("test"), 3)
check: assert l.bufferIs("te")
l.bufferIs("te") assert l.isFull
l.isFull assert l.read == 't'
l.read == 't' assert l.read == 'e'
l.read == 'e' assert l.read == 's'
l.read == 's' assert l.bufferIs("st")
l.bufferIs("st") assert l.read == 't'
l.read == 't'
test "isFull correctness": # "isFull correctness":
block:
var l = VCardLexer( var l = VCardLexer(
pos: 0, pos: 0,
bookmark: -1, bookmark: -1,
@ -251,104 +246,102 @@ suite "vcard/lexer":
# s e # s e
# 0 1 2 3 4 5 6 7 8 9 # 0 1 2 3 4 5 6 7 8 9
check l.isFull assert l.isFull
# s p e # s p e
# 0 1 2 3 4 5 6 7 8 9 # 0 1 2 3 4 5 6 7 8 9
discard l.read discard l.read
check not l.isFull assert not l.isFull
# e s # e s
# 0 1 2 3 4 5 6 7 8 9 # 0 1 2 3 4 5 6 7 8 9
l.bufStart = 3 l.bufStart = 3
l.pos = 3 l.pos = 3
l.bufEnd = 2 l.bufEnd = 2
check l.isFull assert l.isFull
# e s p # e s p
# 0 1 2 3 4 5 6 7 8 9 # 0 1 2 3 4 5 6 7 8 9
discard l.read discard l.read
check: assert l.pos == 4
l.pos == 4 assert not l.isFull
not l.isFull
# e s # e s
# 0 1 2 3 4 5 6 7 8 9 # 0 1 2 3 4 5 6 7 8 9
l.bufStart = 9 l.bufStart = 9
l.pos = 9 l.pos = 9
l.bufEnd = 8 l.bufEnd = 8
check l.isFull assert l.isFull
# p e s # p e s
# 0 1 2 3 4 5 6 7 8 9 # 0 1 2 3 4 5 6 7 8 9
discard l.read discard l.read
check: assert l.pos == 0
l.pos == 0 assert not l.isFull
not l.isFull
test "handles wrapped lines": # "handles wrapped lines":
block:
var l: VCardLexer var l: VCardLexer
l.open(newStringStream("line\r\n wrap\r\nline 2"), 3) l.open(newStringStream("line\r\n wrap\r\nline 2"), 3)
check l.readExpected("line wrap\r\nline 2") assert l.readExpected("line wrap\r\nline 2")
test "fillBuffer correctness": # "fillBuffer correctness":
block:
var l: VCardLexer var l: VCardLexer
l.open(newStringStream(longTestString), 5) l.open(newStringStream(longTestString), 5)
check: assert l.bufferIs(longTestString[0..<4])
l.bufferIs(longTestString[0..<4]) assert l.isFull
l.isFull assert l.bufStart == 0
l.bufStart == 0 assert l.bufEnd == 4
l.bufEnd == 4 assert l.pos == 0
l.pos == 0 assert l.readExpected("Th")
l.readExpected("Th") assert not l.isFull
not l.isFull assert not l.atEnd
not l.atEnd assert l.pos == 2
l.pos == 2
l.fillBuffer l.fillBuffer
check: assert l.isFull
l.isFull assert l.bufEnd == 1
l.bufEnd == 1 assert l.pos == 2
l.pos == 2 assert l.bufStart == 2
l.bufStart == 2
test "bookmark preserves the buffer": # "bookmark preserves the buffer":
block:
var l: VCardLexer var l: VCardLexer
l.open(newStringStream(longTestString), 7) l.open(newStringStream(longTestString), 7)
check: assert l.buffer.len == 7
l.buffer.len == 7 assert l.bufferIs(longTestString[0..<6])
l.bufferIs(longTestString[0..<6]) assert l.isFull
l.isFull assert l.bufEnd == 6
l.bufEnd == 6 assert l.pos == 0
l.pos == 0 assert l.bookmark == -1
l.bookmark == -1 assert l.readExpected(longTestString[0..<5])
l.readExpected(longTestString[0..<5]) assert not l.isFull
not l.isFull assert not l.atEnd
not l.atEnd assert l.pos == 5
l.pos == 5
l.setBookmark l.setBookmark
# read enough to require us to refill the buffer. # read enough to require us to refill the buffer.
check: assert l.bookmark == 5
l.bookmark == 5 assert l.readExpected(longTestString[5..<10])
l.readExpected(longTestString[5..<10]) assert l.pos == 3
l.pos == 3 assert newStartIdx(l) == 5
newStartIdx(l) == 5 assert l.buffer.len == 7
l.buffer.len == 7
l.returnToBookmark l.returnToBookmark
check: assert l.bookmark == -1
l.bookmark == -1 assert l.pos == 5
l.pos == 5
test "readRune": # "readRune":
block:
var l: VCardLexer var l: VCardLexer
l.open(newStringStream("TEST")) l.open(newStringStream("TEST"))
check: assert l.bufferIs("TEST")
l.bufferIs("TEST") assert l.peekRune == Rune('T')
l.peekRune == Rune('T') assert l.readRune == Rune('T')
l.readRune == Rune('T') assert l.readRune == Rune('E')
l.readRune == Rune('E') assert l.readRune == Rune('S')
l.readRune == Rune('S') assert l.readRune == Rune('T')
l.readRune == Rune('T')
when isMainModule: runVcardLexerTests()

View File

@ -14,7 +14,7 @@ import std/[base64, macros, options, sequtils, streams, strutils, times,
import zero_functional import zero_functional
import vcard/private/[util, lexer] import ./vcard/private/[util, lexer]
type type
VC3_ValueTypes = enum VC3_ValueTypes = enum
@ -381,7 +381,7 @@ func newVC3_Tel*(
telType = @[$ttVoice], telType = @[$ttVoice],
group = none[string]()): VC3_Tel = group = none[string]()): VC3_Tel =
return VC3_Tel(name: "TEL", telType: telType, group: group) return assignFields(VC3_Tel(name: "TEL"), value, telType, group)
func newVC3_Email*( func newVC3_Email*(
value: string, value: string,
@ -1712,7 +1712,7 @@ proc parseContentLines(p: var VC3Parser): seq[VC3_Content] =
lat = parseFloat(partsStr[0]), lat = parseFloat(partsStr[0]),
long = parseFloat(partsStr[1]) long = parseFloat(partsStr[1])
)) ))
except: except ValueError:
p.error("expected two float values separated by ';' for the GEO " & p.error("expected two float values separated by ';' for the GEO " &
"content type but received '" & rawValue & "'") "content type but received '" & rawValue & "'")
@ -1881,144 +1881,147 @@ stateDiagram-v2
## Private Function Unit Tests ## Private Function Unit Tests
## ============================================================================ ## ============================================================================
proc runVcard3PrivateTests*() =
import std/unittest
suite "vcard/vcard3/private":
proc initParser(input: string): VC3Parser = proc initParser(input: string): VC3Parser =
result = VC3Parser(filename: "private unittests") result = VC3Parser(filename: "private unittests")
lexer.open(result, newStringStream(input)) lexer.open(result, newStringStream(input))
test "readGroup with group": # "vcard/vcard3/private"
block:
var p = initParser("mygroup.BEGIN:VCARD") var p = initParser("mygroup.BEGIN:VCARD")
let g = p.readGroup let g = p.readGroup
assert g.isSome
assert g.get == "mygroup"
check: # "readGroup without group":
g.isSome block:
g.get == "mygroup"
test "readGroup without group":
var p = initParser("BEGIN:VCARD") var p = initParser("BEGIN:VCARD")
check p.readGroup.isNone assert p.readGroup.isNone
test "expect (case-sensitive)": # "expect (case-sensitive)":
block:
var p = initParser("BEGIN:VCARD") var p = initParser("BEGIN:VCARD")
p.expect("BEGIN", true) p.expect("BEGIN", true)
try: try:
p.expect(":vcard", true) p.expect(":vcard", true)
check "" == "expect should have raised an error" assert "" == "expect should have raised an error"
except: discard except CatchableError: discard
test "expect (case-insensitive)": # "expect (case-insensitive)":
block:
var p = initParser("BEGIN:VCARD") var p = initParser("BEGIN:VCARD")
p.expect("begin") p.expect("begin")
try: try:
p.expect("begin") p.expect("begin")
check "" == "expect should have raised an error" assert "" == "expect should have raised an error"
except: discard except CatchableError: discard
test "readName": # "readName":
block:
var p = initParser("TEL;tel;x-Example;x-Are1+Name") var p = initParser("TEL;tel;x-Example;x-Are1+Name")
check: assert p.readName == "TEL"
p.readName == "TEL" assert p.read == ';'
p.read == ';' assert p.readName == "TEL"
p.readName == "TEL" assert p.read == ';'
p.read == ';' assert p.readName == "X-EXAMPLE"
p.readName == "X-EXAMPLE" assert p.read == ';'
p.read == ';' assert p.readName == "X-ARE1"
p.readName == "X-ARE1"
try: try:
discard p.readName discard p.readName
check "" == "readName should have raised an error" assert "" == "readName should have raised an error"
except: discard except CatchableError: discard
test "readParamValue": # "readParamValue":
block:
var p = initParser("TEL;TYPE=WORK;TYPE=Fun&Games%:+15551234567") var p = initParser("TEL;TYPE=WORK;TYPE=Fun&Games%:+15551234567")
check: assert p.readName == "TEL"
p.readName == "TEL" assert p.read == ';'
p.read == ';' assert p.readName == "TYPE"
p.readName == "TYPE" assert p.read == '='
p.read == '=' assert p.readParamValue == "WORK"
p.readParamValue == "WORK" assert p.read == ';'
p.read == ';' assert p.readName == "TYPE"
p.readName == "TYPE" assert p.read == '='
p.read == '=' assert p.readParamValue == "Fun&Games%"
p.readParamValue == "Fun&Games%"
test "readParams": # "readParams":
block:
var p = initParser("TEL;TYPE=WORK;TYPE=Fun&Games%,Extra:+15551234567") var p = initParser("TEL;TYPE=WORK;TYPE=Fun&Games%,Extra:+15551234567")
check p.readName == "TEL" assert p.readName == "TEL"
let params = p.readParams let params = p.readParams
check: assert params.len == 2
params.len == 2 assert params[0].name == "TYPE"
params[0].name == "TYPE" assert params[0].values.len == 1
params[0].values.len == 1 assert params[0].values[0] == "WORK"
params[0].values[0] == "WORK" assert params[1].name == "TYPE"
params[1].name == "TYPE" assert params[1].values.len == 2
params[1].values.len == 2 assert params[1].values[0] == "Fun&Games%"
params[1].values[0] == "Fun&Games%" assert params[1].values[1] == "Extra"
params[1].values[1] == "Extra"
test "readValue": # "readValue":
block:
var p = initParser("TEL;TYPE=WORK:+15551234567\r\nFN:John Smith\r\n") var p = initParser("TEL;TYPE=WORK:+15551234567\r\nFN:John Smith\r\n")
check p.skip("TEL") assert p.skip("TEL")
discard p.readParams discard p.readParams
check p.read == ':' assert p.read == ':'
check p.readValue == "+15551234567" assert p.readValue == "+15551234567"
p.expect("\r\n") p.expect("\r\n")
check p.readName == "FN" assert p.readName == "FN"
discard p.readParams discard p.readParams
check p.read == ':' assert p.read == ':'
check p.readValue == "John Smith" assert p.readValue == "John Smith"
test "readTextValueList": # "readTextValueList":
block:
var p = initParser("Public;John;Quincey,Adams;Rev.;Esq:limited\r\n") var p = initParser("Public;John;Quincey,Adams;Rev.;Esq:limited\r\n")
check: assert p.readTextValueList == @["Public"]
p.readTextValueList == @["Public"] assert p.readTextValueList(ifPrefix = some(';')) == @["John"]
p.readTextValueList(ifPrefix = some(';')) == @["John"] assert p.readTextValueList(ifPrefix = some(';')) == @["Quincey", "Adams"]
p.readTextValueList(ifPrefix = some(';')) == @["Quincey", "Adams"] assert p.readTextValueList(ifPrefix = some(';')) == @["Rev."]
p.readTextValueList(ifPrefix = some(';')) == @["Rev."] assert p.readTextValueList(ifPrefix = some(';')) == @["Esq:limited"]
p.readTextValueList(ifPrefix = some(';')) == @["Esq:limited"] assert p.readTextValueList(ifPrefix = some(';')) == newSeq[string]()
p.readTextValueList(ifPrefix = some(';')) == newSeq[string]()
test "existsWithValue": # "existsWithValue":
block:
var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL") var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL")
let params = p.readParams let params = p.readParams
check: assert params.existsWithValue("TYPE", "WORK")
params.existsWithValue("TYPE", "WORK") assert params.existsWithValue("TYPE", "CELL")
params.existsWithValue("TYPE", "CELL") assert not params.existsWithValue("TYPE", "ISDN")
not params.existsWithValue("TYPE", "ISDN")
test "getSingleValue": # "getSingleValue":
block:
var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL") var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL")
let params = p.readParams let params = p.readParams
let val = params.getSingleValue("TYPE") let val = params.getSingleValue("TYPE")
check: assert val.isSome
val.isSome assert val.get == "WORK"
val.get == "WORK" assert params.getSingleValue("VALUE").isNone
params.getSingleValue("VALUE").isNone
test "getMultipleValues": # "getMultipleValues":
block:
var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL") var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL")
let params = p.readParams let params = p.readParams
check: assert params.getMultipleValues("TYPE") == @["WORK", "VOICE", "CELL"]
params.getMultipleValues("TYPE") == @["WORK", "VOICE", "CELL"] assert params.getMultipleValues("VALUE") == newSeq[string]()
params.getMultipleValues("VALUE") == newSeq[string]()
test "validateNoParameters": # "validateNoParameters":
block:
var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL") var p = initParser(";TYPE=WORK;TYPE=VOICE;TYPE=CELL")
let params = p.readParams let params = p.readParams
p.validateNoParameters(@[], "TEST") p.validateNoParameters(@[], "TEST")
try: try:
p.validateNoParameters(params, "TEST") p.validateNoParameters(params, "TEST")
check "" == "validateNoParameters should have errored" assert "" == "validateNoParameters should have errored"
except: discard except CatchableError: discard
test "validateRequredParameters": # "validateRequredParameters":
block:
var p = initParser(";CONTEXT=word;VALUE=uri;TYPE=CELL") var p = initParser(";CONTEXT=word;VALUE=uri;TYPE=CELL")
let params = p.readParams let params = p.readParams
p.validateRequiredParameters(params, p.validateRequiredParameters(params,
@ -2026,5 +2029,7 @@ suite "vcard/vcard3/private":
try: try:
p.validateRequiredParameters(params, [("TYPE", "VOICE")]) p.validateRequiredParameters(params, [("TYPE", "VOICE")])
check "" == "validateRequiredParameters should have errored" assert "" == "validateRequiredParameters should have errored"
except: discard except CatchableError: discard
when isMainModule: runVcard3PrivateTests()

View File

@ -1,6 +1,6 @@
BEGIN:VCARD BEGIN:VCARD
PRODID:-//CyrusIMAP.org//Cyrus 3.7.0-alpha0-927-gf4c98c8499-fm-202208..//EN
VERSION:3.0 VERSION:3.0
PRODID:-//CyrusIMAP.org//Cyrus 3.7.0-alpha0-927-gf4c98c8499-fm-202208..//EN
UID:cdaf67dc-b702-41ac-9c26-bb61df3032d2 UID:cdaf67dc-b702-41ac-9c26-bb61df3032d2
N:Bernard;Jonathan;;; N:Bernard;Jonathan;;;
FN:Jonathan Bernard FN:Jonathan Bernard

View File

@ -1,2 +1,6 @@
import unittest import unittest
import vcard/private/lexer import ./vcard/private/lexer
suite "vcard/private/lexer":
test "private lexer tests":
runVcardLexerPrivateTests()

View File

@ -1,24 +1,47 @@
import options, unittest, vcard3, zero_functional import options, unittest, zero_functional
import ./vcard
suite "vcard/vcard3": suite "vcard/vcard3":
let testVCard = test "vcard3/private tests":
"BEGIN:VCARD\r\n" & runVcard3PrivateTests()
"VERSION:3.0\r\n" &
"FN:Mr. John Q. Public\\, Esq.\r\n" &
"N:Public;John;Quinlan;Mr.;Esq.\r\n" &
"END:VCARD\r\n"
test "minimal VCard": let jdbVCard = readFile("tests/jdb.vcf")
let vc = parseVCard3(testVCard)[0] let jdb = parseVCard3(jdbVCard)[0]
test "parseVCard3":
check: check:
vc.n.family[0] == "Public" jdb.n.family == @["Bernard"]
vc.n.given[0] == "John" jdb.n.given == @["Jonathan"]
vc.fn.value == "Mr. John Q. Public\\, Esq." jdb.fn.value == "Jonathan Bernard"
test "serialize minimal VCard": test "parseVCard3File":
let vc = parseVCard3(testVCard)[0] let jdb = parseVCard3File("tests/jdb.vcf")[0]
check $vc == testVCard check:
jdb.email.len == 7
jdb.email[0].value == "jonathan@jdbernard.com"
jdb.email[0].emailType.contains("pref")
jdb.fn.value == "Jonathan Bernard"
test "email is parsed correctly":
check:
jdb.email.len == 7
jdb.email[0].value == "jonathan@jdbernard.com"
jdb.email[0].emailType.contains("pref")
jdb.email[0].emailType.contains("home")
jdb.email[1].value == "jdb@jdb-software.com"
jdb.email[1].emailType.contains("work")
jdb.email[2].group.isSome
jdb.email[2].group.get == "email2"
jdb.email[6].value == "jbernard@vectra.ai"
jdb.email[6].emailType.contains("work")
test "tel is parsed correctly":
check:
jdb.tel.len == 2
jdb.tel[0].value == "(512) 777-1602"
jdb.tel[0].telType.contains("CELL")
test "RFC2426 Author's VCards": test "RFC2426 Author's VCards":
let vcardsStr = let vcardsStr =
@ -53,12 +76,3 @@ suite "vcard/vcard3":
vcards[0].fn.value == "Frank Dawson" vcards[0].fn.value == "Frank Dawson"
vcards[0].email.len == 2 vcards[0].email.len == 2
(vcards[0].email --> find(it.emailType.contains("PREF"))).isSome (vcards[0].email --> find(it.emailType.contains("PREF"))).isSome
test "Jonathan Bernard VCard":
#const jdbVcard = readFile("tests/jdb.vcf")
let jdb = parseVCard3File("tests/jdb.vcf")[0]
check:
jdb.email.len == 7
jdb.email[0].value == "jonathan@jdbernard.com"
jdb.email[0].emailType.contains("pref")
jdb.fn.value == "Jonathan Bernard"

View File

@ -1,6 +1,6 @@
# Package # Package
version = "0.1.2" version = "0.1.3"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Nim parser for the vCard format version 3.0 (4.0 planned)." description = "Nim parser for the vCard format version 3.0 (4.0 planned)."
license = "MIT" license = "MIT"