From eee6d1979be1fa457cb3e61155b1d3a36a453f62 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 18 Sep 2025 15:21:49 +1200 Subject: [PATCH] [FileFormats.LP] add explicit tests for the ParseError messages --- src/FileFormats/LP/read.jl | 95 +++-- test/FileFormats/LP/LP.jl | 363 ++++++++++++------ test/FileFormats/LP/models/invalid_bound_2.lp | 10 +- .../LP/models/invalid_constraint.lp | 26 +- .../LP/models/invalid_variable_name.lp | 26 +- 5 files changed, 352 insertions(+), 168 deletions(-) diff --git a/src/FileFormats/LP/read.jl b/src/FileFormats/LP/read.jl index dc9df9b645..6dc74a3cb1 100644 --- a/src/FileFormats/LP/read.jl +++ b/src/FileFormats/LP/read.jl @@ -82,11 +82,7 @@ function Base.read!(io::IO, model::Model{T}) where {T} "No file contents are allowed after `end`.", ) else - _throw_parse_error( - state, - token, - "Parsing this section is not supported by the current reader.", - ) + _expect(state, token, _TOKEN_KEYWORD) end end # if keyword != :END @@ -190,8 +186,8 @@ This dictionary makes `_TokenKind` to a string that is used when printing error messages. The string must complete the sentence "We expected this token to be ". """ const _KIND_TO_MSG = Dict{_TokenKind,String}( - _TOKEN_KEYWORD => "a keyword", - _TOKEN_IDENTIFIER => "a variable name", + _TOKEN_KEYWORD => "a keyword defining a new section", + _TOKEN_IDENTIFIER => "an identifier", _TOKEN_NUMBER => "a number", _TOKEN_ADDITION => "the symbol `+`", _TOKEN_SUBTRACTION => "the symbol `-`", @@ -206,7 +202,7 @@ const _KIND_TO_MSG = Dict{_TokenKind,String}( _TOKEN_COLON => "the symbol `:`", _TOKEN_IMPLIES => "the symbol `->`", _TOKEN_NEWLINE => "a new line", - _TOKEN_UNKNOWN => "some unknown symbol", + _TOKEN_UNKNOWN => "a token", ) """ @@ -296,14 +292,50 @@ struct ParseError <: Exception msg::String end +_is_utf8_start(b::UInt8) = b < 0x80 || (0xC0 <= b <= 0xF7) + +function _get_line_about_pos(io::IO, pos::Int, width::Int) + seek(io, max(0, pos - width)) + # This byte might be an invalid or continuation byte. We need to seek + # forward until we reach a new valid byte. + while !_is_utf8_start(peek(io, UInt8)) + read(io, UInt8) + end + char = Char[] + mark = 0 + while !eof(io) && position(io) <= pos + width + c = read(io, Char) + if c == '\n' + if position(io) < pos + empty!(char) + else + break + end + elseif c != '\r' + push!(char, c) + end + if position(io) == pos + mark = length(char) + end + end + if mark == 0 + mark = length(char) + 1 + end + return String(char), mark +end + function _throw_parse_error(state::_LexerState, token::_Token, msg::String) - offset = min(40, token.pos) - seek(state.io, token.pos - offset) - line = String(read(state.io, 2 * offset)) - i = something(findprev('\n', line, offset-1), 0) - j = something(findnext('\n', line, offset), length(line) + 1) - extract = replace(line[(i+1):(j-1)], "\r" => "") - help = string(extract, "\n", " "^(offset - i + - 1), "^\n", msg) + line, mark = _get_line_about_pos(state.io, token.pos, 40) + help = string( + line, + "\n", + " "^(mark - 1), + "^\nGot ", + _KIND_TO_MSG[token.kind], + _with_value(token.value), + ". ", + msg, + ) return throw(ParseError(state.line, help)) end @@ -311,12 +343,15 @@ function Base.showerror(io::IO, err::ParseError) return print(io, "Error parsing LP file on line $(err.line):\n", err.msg) end +_with_value(::Nothing) = "" +_with_value(x::String) = string(" with value `", x, "`") + function _expect(state::_LexerState, token::_Token, kind::_TokenKind) if token.kind != kind _throw_parse_error( state, token, - string("We expected this token to be ", _KIND_TO_MSG[kind]), + "We expected this token to be $(_KIND_TO_MSG[kind]).", ) end return token @@ -346,6 +381,9 @@ function Base.read(state::_LexerState, ::Type{_Token}) end popfirst!(state.peek_tokens) state.current_token = token + if token.kind == _TOKEN_NEWLINE + state.line += 1 + end return token end @@ -445,7 +483,6 @@ function _peek_inner(state::_LexerState) while (c = peek(state, Char)) !== nothing pos = position(state.io) if c == '\n' - state.line += 1 _ = read(state, Char) return _Token(_TOKEN_NEWLINE, nothing, pos) elseif isspace(c) # Whitespace @@ -480,11 +517,17 @@ function _peek_inner(state::_LexerState) _ = read(state, Char) # Allow <=, >=, and == end return _Token(op, nothing, pos) + elseif _is_identifier(c) && !_is_starting_identifier(c) + _throw_parse_error( + state, + _Token(_TOKEN_UNKNOWN, "$c", pos), + "This character is not supported at the start of an identifier.", + ) else _throw_parse_error( state, _Token(_TOKEN_UNKNOWN, "$c", pos), - "This character is not supported an LP file.", + "This character is not supported in an LP file.", ) end end @@ -574,7 +617,11 @@ function _parse_number(state::_LexerState, cache::_ReadCache{T})::T where {T} _expect(state, token, _TOKEN_NUMBER) ret = tryparse(T, token.value) if ret === nothing - _throw_parse_error(state, token, "We expected this to be a number.") + _throw_parse_error( + state, + _Token(_TOKEN_IDENTIFIER, token.value, token.pos), + "We were unable to parse this as a number.", + ) end return ret end @@ -751,7 +798,7 @@ function _parse_term( return _throw_parse_error( state, token, - "Got $(_KIND_TO_MSG[token.kind]), but we expected this to be a new term in the expression.", + "We expected this to be a new term in the expression.", ) end @@ -815,9 +862,7 @@ function _parse_set_suffix(state, cache) p = read(state, _Token) if _compare_case_insenstive(p, "free") return nothing - end - _skip_newlines(state) - if p.kind == _TOKEN_GREATER_THAN + elseif p.kind == _TOKEN_GREATER_THAN rhs = _parse_number(state, cache) return MOI.GreaterThan(rhs) elseif p.kind == _TOKEN_LESS_THAN @@ -830,7 +875,7 @@ function _parse_set_suffix(state, cache) _throw_parse_error( state, p, - "We expected this to be an inequality like `>=`, `<=` ,or `==`.", + "We expected this to be an inequality like `>=`, `<=`, or `==`.", ) end end @@ -856,7 +901,7 @@ function _parse_set_prefix(state, cache) _throw_parse_error( state, p, - "We expected this to be an inequality like `>=`, `<=` ,or `==`.", + "We expected this to be an inequality like `>=`, `<=`, or `==`.", ) end end diff --git a/test/FileFormats/LP/LP.jl b/test/FileFormats/LP/LP.jl index 56545ccc46..02c183fff8 100644 --- a/test/FileFormats/LP/LP.jl +++ b/test/FileFormats/LP/LP.jl @@ -245,9 +245,7 @@ end function test_free_variables_reading() for case in ["free", "Free", "FreE", "frEe"] - io = IOBuffer() - write(io, "Minimize\nobj: x\nsubject to\nBounds\nx $case\nEnd") - seekstart(io) + io = IOBuffer("Minimize\nobj: x\nsubject to\nBounds\nx $case\nEnd") model = MOI.FileFormats.LP.Model() read!(io, model) out = IOBuffer() @@ -261,9 +259,7 @@ end function test_integer_variables_reading() for case in ["general", "GeneRalS", "Integer", "InTegeRs"] - io = IOBuffer() - write(io, "Minimize\nobj: x\nsubject to\n$case\nx\nEnd") - seekstart(io) + io = IOBuffer("Minimize\nobj: x\nsubject to\n$case\nx\nEnd") model = MOI.FileFormats.LP.Model() read!(io, model) out = IOBuffer() @@ -441,25 +437,87 @@ end ### Read tests ### -function test_read_invalid() - models = joinpath(@__DIR__, "models") - for filename in filter(f -> startswith(f, "invalid_"), readdir(models)) - model = LP.Model() - @test_throws( - LP.ParseError, - MOI.read_from_file(model, joinpath(models, filename)), - ) +macro test_parse_error(result, expr) + return quote + ret = try + $(esc(expr)) + catch err + sprint(showerror, err) + end + @test ret == $(esc(result)) end +end + +function test_read_invalid() + dir = joinpath(@__DIR__, "models") + @test_parse_error( + """ + Error parsing LP file on line 7: + C: 1 x <= 2 + ^ + Got an identifier with value `C`. No file contents are allowed after `end`.""", + MOI.read_from_file(LP.Model(), joinpath(dir, "invalid_after_end.lp")), + ) + @test_parse_error( + """ + Error parsing LP file on line 6: + x1 != 10 + ^ + Got an identifier with value `!`. We expected this to be an inequality like `>=`, `<=`, or `==`.""", + MOI.read_from_file(LP.Model(), joinpath(dir, "invalid_bound_2.lp")), + ) + @test_parse_error( + """ + Error parsing LP file on line 6: + c2: 2 x1 + x2 + c2: + 3 x3 + x4 >= 15 + ^ + Got the symbol `:`. We expected this to be an inequality like `>=`, `<=`, or `==`.""", + MOI.read_from_file(LP.Model(), joinpath(dir, "invalid_constraint.lp")), + ) + @test_parse_error( + """ + Error parsing LP file on line 10: + csos2: S2:: + ^ + Got a new line. SOS constraints cannot be spread across lines.""", + MOI.read_from_file( + LP.Model(), + joinpath(dir, "invalid_sos_constraint.lp"), + ), + ) + @test_parse_error( + """ + Error parsing LP file on line 10: + csos2: S3:: V2:2 V4:1 V5:2.5 + ^ + Got an identifier with value `S3`. This must be either `S1` for SOS-I or `S2` for SOS-II.""", + MOI.read_from_file(LP.Model(), joinpath(dir, "invalid_sos_set.lp")), + ) + @test_parse_error( + """ + Error parsing LP file on line 3: + obj: 3 1x1 + x2 + 5 x3 + x4 + ^ + Got a number with value `1`. We expected this token to be a keyword defining a new section.""", + MOI.read_from_file( + LP.Model(), + joinpath(dir, "invalid_variable_name.lp"), + ), + ) return end function test_read_unexpected_line() - io = IOBuffer() - line = "MinimizeSubject to x + y = 0" - print(io, line) - seekstart(io) + io = IOBuffer("MinimizeSubject to x + y = 0") model = LP.Model() - @test_throws LP.ParseError read!(io, model) + @test_parse_error( + """ + Error parsing LP file on line 1: + MinimizeSubject to x + y = 0 + ^ + Got an identifier with value `MinimizeSubject`. We expected this token to be a keyword defining a new section.""", + read!(io, model), + ) return end @@ -590,7 +648,6 @@ function test_read_objective_sense() for (sense, result) in cases model = LP.Model() io = IOBuffer("$sense\nx") - seekstart(io) read!(io, model) @test MOI.get(model, MOI.ObjectiveSense()) == result end @@ -620,14 +677,12 @@ function test_read_maximum_length_error() Error parsing LP file on line 2: obj: - 2 - 1 V4 + 1 V5 + 3 + 2 - 0.5 ^ - Name (V4) exceeds maximum length (1)""" + Got an identifier with value `V4`. Name (V4) exceeds maximum length (1)""" return end function test_default_bound() - io = IOBuffer() - write(io, "minimize\nobj: x + y") - seekstart(io) + io = IOBuffer("minimize\nobj: x + y") model = LP.Model() MOI.read!(io, model) x = MOI.get(model, MOI.ListOfVariableIndices()) @@ -640,9 +695,7 @@ function test_default_bound() end function test_default_bound_double_bound() - io = IOBuffer() - write(io, "minimize\nobj: x\nsubject to\nbounds\n x <= -1\n x >= -2") - seekstart(io) + io = IOBuffer("minimize\nobj: x\nsubject to\nbounds\n x <= -1\n x >= -2") model = LP.Model() MOI.read!(io, model) x = first(MOI.get(model, MOI.ListOfVariableIndices())) @@ -715,9 +768,7 @@ function test_quadratic_newline_edge_cases() "+ [ x^2\n]\n/2 \\ comment\n", "+ \n[ x^2\n]\n/2 \\ comment\n", ] - io = IOBuffer() - write(io, "Minimize\nobj: x $(case)\nEnd") - seekstart(io) + io = IOBuffer("Minimize\nobj: x $(case)\nEnd") model = MOI.FileFormats.LP.Model() read!(io, model) out = IOBuffer() @@ -740,9 +791,7 @@ function test_quadratic_newline_edge_cases() "+ [ x^2\n] \\ comment", "+ \n[ x^2\n] \\ comment", ] - io = IOBuffer() - write(io, "Minimize\nobj: x $(case)\nEnd") - seekstart(io) + io = IOBuffer("Minimize\nobj: x $(case)\nEnd") model = MOI.FileFormats.LP.Model() read!(io, model) out = IOBuffer() @@ -762,9 +811,7 @@ function test_newline_inequality() "x +\n y <= 2", "x\n+ y <= 2", ] - io = IOBuffer() - write(io, "Minimize\nobj: x\nSubject to\nc1: $(case)\nEnd") - seekstart(io) + io = IOBuffer("Minimize\nobj: x\nSubject to\nc1: $(case)\nEnd") model = MOI.FileFormats.LP.Model() read!(io, model) out = IOBuffer() @@ -791,9 +838,7 @@ function test_wrong_way_bounds() "2 < x" => "x >= 2", "2 = x" => "x = 2", ] - io = IOBuffer() - write(io, "Minimize\nobj: x\nSubject to\nBounds\n$(case)\nEnd") - seekstart(io) + io = IOBuffer("Minimize\nobj: x\nSubject to\nBounds\n$(case)\nEnd") model = MOI.FileFormats.LP.Model() read!(io, model) out = IOBuffer() @@ -806,9 +851,7 @@ function test_wrong_way_bounds() end function test_variable_coefficient_variable() - io = IOBuffer() - write(io, "Minimize\nobj: x -1 y\nEnd") - seekstart(io) + io = IOBuffer("Minimize\nobj: x -1 y\nEnd") model = MOI.FileFormats.LP.Model() read!(io, model) out = IOBuffer() @@ -1084,18 +1127,13 @@ end function test_invalid_token_in_sos() model = LP.Model() - io = IOBuffer() - print( - io, - """ - minimize - obj: x + y - subject to - SOS - c11: S1:: x 1.0 y 2.0 - """, - ) - seekstart(io) + io = IOBuffer(""" + minimize + obj: x + y + subject to + SOS + c11: S1:: x 1.0 y 2.0 + """) contents = try read!(io, model) catch err @@ -1105,21 +1143,28 @@ function test_invalid_token_in_sos() Error parsing LP file on line 5: c11: S1:: x 1.0 y 2.0 ^ - We expected this token to be the symbol `:`""" + Got a number with value `1.0`. We expected this token to be the symbol `:`.""" return end function test_unable_to_parse_bound() io = IOBuffer(""" - minimize - obj: 1 x - subject to - bounds - x - end - """) + minimize + obj: 1 x + subject to + bounds + x + end + """) model = LP.Model() - @test_throws LP.ParseError read!(io, model) + @test_parse_error( + """ + Error parsing LP file on line 6: + end + ^ + Got a keyword defining a new section with value `END`. We expected this to be an inequality like `>=`, `<=`, or `==`.""", + read!(io, model), + ) return end @@ -1234,7 +1279,14 @@ function test_subject_to_name() seekstart(io) model = MOI.FileFormats.LP.Model() if err - @test_throws LP.ParseError read!(io, model) + @test_parse_error( + """ + Error parsing LP file on line 3: + $(first(split(case, "\n"))) + ^ + Got an identifier with value `$(first(split(case)))`. We expected this token to be a keyword defining a new section.""", + read!(io, model), + ) else read!(io, model) out = IOBuffer() @@ -1260,17 +1312,39 @@ function test_parse_identifier() "aAc2", ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) x = LP._parse_identifier(state, cache) @test cache.variable_name_to_index[input] == x end - for input in ["2", "2x", ".x"] - io = IOBuffer(input) - seekstart(io) - state = LP._LexerState(io) - @test_throws LP.ParseError LP._parse_identifier(state, cache) + for input in ["2", "2x"] + state = LP._LexerState(IOBuffer(input)) + @test_parse_error( + """ + Error parsing LP file on line 1: + $(input) + ^ + Got a number with value `2`. We expected this token to be an identifier.""", + LP._parse_identifier(state, cache), + ) end + state = LP._LexerState(IOBuffer(".x")) + @test_parse_error( + """ + Error parsing LP file on line 1: + .x + ^ + Got a token with value `.`. This character is not supported at the start of an identifier.""", + LP._parse_identifier(state, cache), + ) + state = LP._LexerState(IOBuffer("❤x")) + @test_parse_error( + """ + Error parsing LP file on line 1: + ❤x + ^ + Got a token with value `❤`. This character is not supported in an LP file.""", + LP._parse_identifier(state, cache), + ) return end @@ -1299,15 +1373,26 @@ function test_parse_number() "1.23E+3" => 1230.0, ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) @test LP._parse_number(state, cache) == result end - for input in ["x", "abc", "ten", "1.1.1", "1eE1"] + for (input, reason) in [ + "x" => "We expected this to be a number.", + "abc" => "We expected this to be a number.", + "ten" => "We expected this to be a number.", + "1.1.1" => "We were unable to parse this as a number.", + "1eE1" => "We were unable to parse this as a number.", + ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) - @test_throws LP.ParseError LP._parse_number(state, cache) + @test_parse_error( + """ + Error parsing LP file on line 1: + $input + ^ + Got an identifier with value `$input`. $reason""", + LP._parse_number(state, cache), + ) end return end @@ -1330,7 +1415,6 @@ function test_parse_quad_term() "2.2 x * x" => 4.4, ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) term = LP._parse_quadratic_term(state, cache, 1.0) x = cache.variable_name_to_index["x"] @@ -1353,7 +1437,6 @@ function test_parse_quad_term() "2.2 x * y" => 2.2, ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) term = LP._parse_quadratic_term(state, cache, 1.0) x = cache.variable_name_to_index["x"] @@ -1363,11 +1446,23 @@ function test_parse_quad_term() term = LP._parse_quadratic_term(state, cache, -1.0) @test term == MOI.ScalarQuadraticTerm(-coef, x, y) end - for input in ["x^", "x^x", "x^0", "x^1", "x^3", "x * 2 * x"] + for (input, reason) in [ + "x^" => " ^\nGot a token with value `EOF`. Unexpected end to the file. We weren't finished yet.", + "x^x" => " ^\nGot an identifier with value `x`. We expected this token to be a number.", + "x^0" => " ^\nGot a number with value `0`. Only `^ 2` is supported.", + "x^1" => " ^\nGot a number with value `1`. Only `^ 2` is supported.", + "x^3" => " ^\nGot a number with value `3`. Only `^ 2` is supported.", + "x * 2 * x" => " ^\nGot a number with value `2`. We expected this token to be an identifier.", + ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) - @test_throws LP.ParseError LP._parse_quadratic_term(state, cache, -1.0) + @test_parse_error( + """ + Error parsing LP file on line 1: + $input + $reason""", + LP._parse_quadratic_term(state, cache, -1.0), + ) end return end @@ -1387,7 +1482,6 @@ function test_parse_term() "3.2 * x" => 3.2, ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) term = LP._parse_term(state, cache, 1.0) x = cache.variable_name_to_index["x"] @@ -1396,23 +1490,38 @@ function test_parse_term() term = LP._parse_term(state, cache, -1.0) @test term == MOI.ScalarAffineTerm(-coef, x) end - for input in ["subject to", ">= 1"] + for (input, reason) in [ + "subject to" => "Got a keyword defining a new section with value `CONSTRAINTS`.", + ">= 1" => "Got the symbol `>=`.", + ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) - @test_throws LP.ParseError LP._parse_term(state, cache, 1.0) + @test_parse_error( + """ + Error parsing LP file on line 1: + $input + ^ + $reason We expected this to be a new term in the expression.""", + LP._parse_term(state, cache, 1.0), + ) end return end function test_parse_quad_expression() cache = LP._ReadCache(LP.Model{Float64}()) - for input in ["x^2", "[ x^2 ]/", "[ x^2 ]/3"] + for (input, reason) in [ + "x^2" => "^\nGot an identifier with value `x`. We expected this token to be the symbol `[`.", + "[ x^2 ]/" => " ^\nGot a token with value `EOF`. Unexpected end to the file. We weren't finished yet.", + "[ x^2 ]/3" => " ^\nGot a number with value `3`. The only supported value here is `] / 2`.", + ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) - @test_throws( - LP.ParseError, + @test_parse_error( + """ + Error parsing LP file on line 1: + $input + $reason""", LP._parse_quadratic_expression(state, cache, 1.0), ) end @@ -1433,16 +1542,19 @@ function test_parse_set_prefix() "1.0 =>" => MOI.LessThan(1.0), ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) @test LP._parse_set_prefix(state, cache) == set end - for input in ["1 ->"] - io = IOBuffer(input) - seekstart(io) - state = LP._LexerState(io) - @test_throws LP.ParseError LP._parse_set_prefix(state, cache) - end + io = IOBuffer("->") + state = LP._LexerState(io) + @test_parse_error( + """ + Error parsing LP file on line 1: + -> + ^ + Got the symbol `->`. We expected this token to be a number.""", + LP._parse_set_prefix(state, cache), + ) return end @@ -1462,16 +1574,19 @@ function test_parse_set_sufffix() "=> 1.0" => MOI.GreaterThan(1.0), ] io = IOBuffer(input) - seekstart(io) state = LP._LexerState(io) @test LP._parse_set_suffix(state, cache) == set end - for input in ["-> 1"] - io = IOBuffer(input) - seekstart(io) - state = LP._LexerState(io) - @test_throws LP.ParseError LP._parse_set_suffix(state, cache) - end + io = IOBuffer("-> 1") + state = LP._LexerState(io) + @test_parse_error( + """ + Error parsing LP file on line 1: + -> 1 + ^ + Got the symbol `->`. We expected this to be an inequality like `>=`, `<=`, or `==`.""", + LP._parse_set_suffix(state, cache), + ) return end @@ -1504,7 +1619,6 @@ function test_new_line_edge_cases() "minimize\nobj: x + y\nsubject to\nc: x <= 1\nend\\comment", ] io = IOBuffer(input) - seekstart(io) model = LP.Model() MOI.read!(io, model) @test sprint(MOI.write, model) == target @@ -1521,7 +1635,6 @@ function test_new_line_edge_cases_sos() "minimize\nobj: x + y\nsubject to\n\nc: S1:: x:1 y:2\\comment\nend", ] io = IOBuffer(input) - seekstart(io) model = LP.Model() MOI.read!(io, model) @test sprint(MOI.write, model) == target @@ -1541,7 +1654,6 @@ function test_new_line_edge_case_fails() "maximize\nobj: x\nsubject to c: x >= 0", ] io = IOBuffer(input) - seekstart(io) model = LP.Model() @test_throws LP.ParseError MOI.read!(io, model) end @@ -1559,7 +1671,6 @@ function test_parse_keyword_edge_cases_identifier_is_keyword() $name free end """) - seekstart(io) model = LP.Model() MOI.read!(io, model) x = only(MOI.get(model, MOI.ListOfVariableIndices())) @@ -1579,9 +1690,15 @@ function test_parse_keyword_subject_to_errors() x free end """) - seekstart(io) model = LP.Model() - @test_throws LP.ParseError MOI.read!(io, model) + @test_parse_error( + """ + Error parsing LP file on line 3: + $line + ^ + Got an identifier with value `subject`. We expected this token to be a keyword defining a new section.""", + MOI.read!(io, model), + ) end return end @@ -1596,7 +1713,6 @@ function test_parse_newline_in_objective_expression() x free end """) - seekstart(io) model = LP.Model() MOI.read!(io, model) x = MOI.get(model, MOI.VariableIndex, "x") @@ -1609,7 +1725,6 @@ end function test_parse_subject_eof() io = IOBuffer("maximize\nobj:\nsubject") - seekstart(io) model = LP.Model() MOI.read!(io, model) x = MOI.get(model, MOI.VariableIndex, "subject") @@ -1619,7 +1734,6 @@ end function test_parse_expr_eof() io = IOBuffer("maximize\nobj: x + 2\n") - seekstart(io) model = LP.Model() MOI.read!(io, model) x = MOI.get(model, MOI.VariableIndex, "x") @@ -1631,7 +1745,6 @@ end function test_parse_quadratic_expr_eof() io = IOBuffer("maximize\nobj: [x * x]\n") - seekstart(io) model = LP.Model() MOI.read!(io, model) x = MOI.get(model, MOI.VariableIndex, "x") @@ -1645,7 +1758,14 @@ function test_ambiguous_case_1() # Xpress allows this. We currently don't. io = IOBuffer("maximize obj: x subject to c: x <= 1 end") model = LP.Model() - @test_throws LP.ParseError MOI.read!(io, model) + @test_parse_error( + """ + Error parsing LP file on line 1: + maximize obj: x subject to c: x <= 1 end + ^ + Got an identifier with value `maximize`. We expected this token to be a keyword defining a new section.""", + MOI.read!(io, model), + ) return end @@ -1674,6 +1794,25 @@ function test_ambiguous_case_3() return end +function test_get_line_about_pos() + for (input, output) in [ + "x" => ("x", 1), + "\nx" => ("x", 1), + "x\n" => ("x", 1), + "x + y\n" => ("x + y", 1), + "\ny + x\n" => ("y + x", 5), + "\n❤ + x\n" => ("❤ + x", 5), + "♡♡♡♡♡♡♡♡♡♡ + x + ♡♡♡♡♡♡♡♡♡♡" => ("♡♡ + x + ♡♡♡", 6), + "♡♡♡♡♡♡♡♡♡\n♡ + x + ♡\n♡♡♡♡♡♡♡♡♡" => ("♡ + x + ♡", 5), + ] + io = IOBuffer(input) + readuntil(io, 'x') + pos = position(io) + @test output == LP._get_line_about_pos(IOBuffer(input), pos, 10) + end + return +end + end # module TestLP.runtests() diff --git a/test/FileFormats/LP/models/invalid_bound_2.lp b/test/FileFormats/LP/models/invalid_bound_2.lp index af32668df1..6c4b334e13 100644 --- a/test/FileFormats/LP/models/invalid_bound_2.lp +++ b/test/FileFormats/LP/models/invalid_bound_2.lp @@ -1,7 +1,7 @@ -maximize +maximize obj: x1 -subject to +subject to c1: x <= 11 -bounds - x1 != 10 -end +bounds + x1 != 10 +end diff --git a/test/FileFormats/LP/models/invalid_constraint.lp b/test/FileFormats/LP/models/invalid_constraint.lp index c7d9610ead..b7ade78474 100644 --- a/test/FileFormats/LP/models/invalid_constraint.lp +++ b/test/FileFormats/LP/models/invalid_constraint.lp @@ -1,13 +1,13 @@ -\ File: lo1.lp -maximize -obj: 3 x1 + x2 + 5 x3 + x4 -subject to -c1: 3 x1 + x2 + 2 x3 = 30 -c2: 2 x1 + x2 + c2: + 3 x3 + x4 >= 15 -c3: 2 x2 + 3 x4 <= 25 -bounds - 0 <= x1 <= +infinity - 0 <= x2 <= 10 - 0 <= x3 <= +infinity - 0 <= x4 <= +infinity -end +\ File: lo1.lp +maximize +obj: 3 x1 + x2 + 5 x3 + x4 +subject to +c1: 3 x1 + x2 + 2 x3 = 30 +c2: 2 x1 + x2 + c2: + 3 x3 + x4 >= 15 +c3: 2 x2 + 3 x4 <= 25 +bounds + 0 <= x1 <= +infinity + 0 <= x2 <= 10 + 0 <= x3 <= +infinity + 0 <= x4 <= +infinity +end diff --git a/test/FileFormats/LP/models/invalid_variable_name.lp b/test/FileFormats/LP/models/invalid_variable_name.lp index 650ac61a92..c40d77991d 100644 --- a/test/FileFormats/LP/models/invalid_variable_name.lp +++ b/test/FileFormats/LP/models/invalid_variable_name.lp @@ -1,13 +1,13 @@ -\ File: lo1.lp -maximize -obj: 3 1x1 + x2 + 5 x3 + x4 -subject to -c1: 3 x1 + x2 + 2 x3 = 30 -c2: 2 x1 + x2 + 3 x3 + x4 >= 15 -c3: 2 x2 + 3 x4 <= 25 -bounds - 0 <= x1 <= +infinity - 0 <= x2 <= 10 - 0 <= x3 <= +infinity - 0 <= x4 <= +infinity -end +\ File: lo1.lp +maximize +obj: 3 1x1 + x2 + 5 x3 + x4 +subject to +c1: 3 x1 + x2 + 2 x3 = 30 +c2: 2 x1 + x2 + 3 x3 + x4 >= 15 +c3: 2 x2 + 3 x4 <= 25 +bounds + 0 <= x1 <= +infinity + 0 <= x2 <= 10 + 0 <= x3 <= +infinity + 0 <= x4 <= +infinity +end