Skip to content

Commit d908f51

Browse files
authored
Fixing failing silently (#28)
* Added errors and updated tests for the following cases: - removing edges between nodes which do not exist. - removing attributes which have not been set. * 1. Added quotations arround node names in errors. 2. When removing edges between nodes in a graph if no edges exist and error is outputted. * - Added function to check if a label is valid. - Reduced some code duplication in other spots TODO: add label validity checking to all functions where labels may be added. * added planning * added attribute removal when setting an existing attribute on graphs, added more tests and went through operations which could fail and reasonably should error * Delete plan.md
1 parent 4df50dd commit d908f51

File tree

8 files changed

+227
-99
lines changed

8 files changed

+227
-99
lines changed

gap/dot.gd

+2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ DeclareOperation("GraphvizAddNode", [IsGraphvizGraphDigraphOrContext, IsObject])
161161
#! If no nodes with the same name are in the graph then the edge's nodes will be
162162
#! added to the graph. If different nodes with the same name are in the graph
163163
#! then the operation fails.
164+
#! TODO I dont believe this is accurate - think it will connect existing ones
165+
#! underlying private function would fail though - TODO double check.
164166
DeclareOperation("GraphvizAddEdge",
165167
[IsGraphvizGraphDigraphOrContext, IsObject, IsObject]);
166168

gap/dot.gi

+65-35
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,9 @@ function(x, name, value)
281281
" be removed using GraphvizRemoveAttr");
282282
fi;
283283

284-
attrs := GraphvizAttrs(x);
285284
name := String(name);
285+
GV_RemoveGraphAttrIfExists(x, name);
286+
attrs := GraphvizAttrs(x);
286287
value := String(value);
287288
if ' ' in value then
288289
# Replace with call to GV_QuoteName or whatever TODO
@@ -297,9 +298,31 @@ end);
297298
InstallMethod(GraphvizSetAttr, "for a graphviz (di)graph or context and object",
298299
[IsGraphvizGraphDigraphOrContext, IsObject],
299300
function(x, value)
300-
local attrs;
301-
attrs := GraphvizAttrs(x);
301+
local attrs, match, pred;
302+
303+
match := function(lookup, target)
304+
local idx, pred;
305+
idx := 1;
302306

307+
pred := function(i)
308+
return i <= Length(target) and i <= Length(lookup)
309+
and lookup[i] = target[i] and lookup[i] <> '=';
310+
end;
311+
312+
while pred(idx) do
313+
idx := idx + 1;
314+
od;
315+
if idx > Length(lookup) or idx > Length(lookup) then
316+
return false;
317+
elif lookup[idx] = '=' and target[idx] = '=' then
318+
return true;
319+
fi;
320+
return false;
321+
end;
322+
323+
attrs := GraphvizAttrs(x);
324+
x!.Attrs := Filtered(attrs, attr -> not match(attr, value));
325+
attrs := GraphvizAttrs(x);
303326
Add(attrs, String(value));
304327
return x;
305328
end);
@@ -464,7 +487,6 @@ function(g, name)
464487
if nodes[name] <> fail then
465488
Unbind(nodes[name]);
466489
else
467-
# Don't just silently do nothing
468490
ErrorFormatted("the 2nd argument (node name string) \"{}\"",
469491
" is not a node of the 1st argument (a graphviz",
470492
" (di)graph/context)",
@@ -511,6 +533,20 @@ InstallMethod(GraphvizRemoveEdges,
511533
"for a graphviz (di)graph or context, string, and string",
512534
[IsGraphvizGraphDigraphOrContext, IsString, IsString],
513535
function(g, hn, tn)
536+
local lh, lt, len;
537+
538+
# if no such nodes exist -> error out
539+
lh := GV_FindNode(g, hn) = fail;
540+
lt := GV_FindNode(g, tn) = fail;
541+
if lh and lt then
542+
ErrorFormatted("no nodes with names \"{}\" or \"{}\"", hn, tn);
543+
elif lh then
544+
ErrorFormatted("no node with name \"{}\"", hn);
545+
elif lt then
546+
ErrorFormatted("no node with name \"{}\"", tn);
547+
fi;
548+
549+
len := Length(GraphvizEdges(g));
514550
GraphvizFilterEdges(g,
515551
function(e)
516552
local head, tail, tmp;
@@ -523,6 +559,10 @@ function(g, hn, tn)
523559
return tmp and (hn <> GraphvizName(tail) or tn <> GraphvizName(head));
524560
fi;
525561
end);
562+
if len - Length(GraphvizEdges(g)) = 0 then
563+
ErrorFormatted("no edges exist from \"{}\" to \"{}\"",
564+
tn, hn);
565+
fi;
526566

527567
return g;
528568
end);
@@ -537,44 +577,33 @@ InstallMethod(GraphvizRemoveAttr, "for a graphviz object and an object",
537577
function(obj, attr)
538578
local attrs;
539579
attrs := GraphvizAttrs(obj);
540-
# TODO error if no such attr?
541-
Unbind(attrs[String(attr)]);
580+
attr := String(attr);
581+
582+
if not IsBound(attrs[attr]) then
583+
ErrorFormatted("the 2nd argument (attribute name) \"{}\" ",
584+
"is not set on the provided object.",
585+
attr);
586+
fi;
587+
588+
Unbind(attrs[attr]);
542589
return obj;
543590
end);
544591

545-
# TODO this doesn't currently work as intended, see:
546-
# https://github.com/digraphs/graphviz/issues/23
547592
InstallMethod(GraphvizRemoveAttr,
548593
"for a graphviz (di)graph or context and an object",
549594
[IsGraphvizGraphDigraphOrContext, IsObject],
550595
function(obj, attr)
551-
local attrs, i, match;
596+
local attrs, len;
552597
attrs := GraphvizAttrs(obj);
553-
attr := String(attr);
554-
555-
# checks if they attribute names match the one being removed
556-
match := function(key, str)
557-
for i in [1 .. Length(key)] do
558-
if i > Length(str) or key[i] <> str[i] then
559-
return false;
560-
fi;
561-
od;
562-
563-
i := i + 1;
564-
while i <= Length(str) do
565-
if str[i] = '=' then
566-
return true;
567-
elif str[i] <> '\s' and str[i] <> '\t' then
568-
return false;
569-
fi;
570-
i := i + 1;
571-
od;
572-
573-
# attributes which are not key value or removal by value
574-
return true;
575-
end;
576-
577-
obj!.Attrs := Filtered(attrs, s -> not match(attr, s));
598+
len := Length(attrs);
599+
600+
GV_RemoveGraphAttrIfExists(obj, attr);
601+
# error if no attributes were removed i.e. did not exist
602+
if Length(obj!.Attrs) - len = 0 then
603+
ErrorFormatted("the 2nd argument (attribute name or attribute) \"{}\" ",
604+
"is not set on the provided object.",
605+
attr);
606+
fi;
578607
return obj;
579608
end);
580609

@@ -608,9 +637,10 @@ function(gv, labels)
608637
"has incorrect length, expected {}, but ",
609638
"found {}", Size(GraphvizNodes(gv)), Size(labels));
610639
fi;
611-
# TODO GV_ErrorIfNotValidLabel
640+
612641
nodes := GraphvizNodes(gv);
613642
for i in [1 .. Size(nodes)] do
643+
GV_ErrorIfNotValidLabel(labels[i]);
614644
GraphvizSetAttr(nodes[i], "label", labels[i]);
615645
od;
616646
return gv;

gap/gv.gd

+4
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ DeclareOperation("GV_AddEdge",
6060
DeclareOperation("GV_GetIdx", [IsGraphvizObject]);
6161
DeclareOperation("GV_ConstructHistory", [IsGraphvizGraphDigraphOrContext]);
6262

63+
DeclareOperation("GV_RemoveGraphAttrIfExists",
64+
[IsGraphvizGraphDigraphOrContext, IsString]);
65+
6366
DeclareGlobalFunction("GV_IsValidColor");
6467
DeclareGlobalFunction("GV_ErrorIfNotNodeColoring");
68+
DeclareGlobalFunction("GV_ErrorIfNotValidLabel");
6569

6670
# TODO move to dot? and make public?
6771
BindGlobal("GV_ObjectFamily",

gap/gv.gi

+94-48
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ end);
259259
InstallMethod(Size, "for a graphviz map",
260260
[GV_IsMap], m -> Length(GV_MapNames(m)));
261261

262-
# ??
262+
# Graph child counter functions
263263

264264
InstallMethod(GV_IncCounter,
265265
"for a graphviz graph",
@@ -400,7 +400,7 @@ function(g, n)
400400
local graph;
401401
graph := GV_FindGraphWithNode(g, n);
402402
if graph = fail then
403-
return graph;
403+
return fail;
404404
fi;
405405
return graph[n];
406406
end);
@@ -464,6 +464,39 @@ function(x, edge)
464464
return x;
465465
end);
466466

467+
InstallMethod(GV_RemoveGraphAttrIfExists,
468+
"for a graphviz graph context or digraph and a string",
469+
[IsGraphvizGraphDigraphOrContext, IsString],
470+
function(obj, attr)
471+
local attrs, i, match;
472+
attrs := GraphvizAttrs(obj);
473+
attr := String(attr);
474+
475+
# checks if they attribute names match the one being removed
476+
match := function(key, str)
477+
for i in [1 .. Length(key)] do
478+
if i > Length(str) or key[i] <> str[i] then
479+
return false;
480+
fi;
481+
od;
482+
483+
i := i + 1;
484+
while i <= Length(str) do
485+
if str[i] = '=' then
486+
return true;
487+
elif str[i] <> '\s' and str[i] <> '\t' then
488+
return false;
489+
fi;
490+
i := i + 1;
491+
od;
492+
493+
# attributes which are not key value or removal by value
494+
return true;
495+
end;
496+
497+
obj!.Attrs := Filtered(attrs, s -> not match(attr, s));
498+
end);
499+
467500
###############################################################################
468501
# Stringifying
469502
###############################################################################
@@ -559,62 +592,42 @@ InstallMethod(GV_StringifyNodeEdgeAttrs,
559592
"for a GV_Map",
560593
[GV_IsMap],
561594
function(attrs)
562-
local result, keys, key, val, n, i, tmp;
595+
local result, keys, key, val, n, i, tmp, format;
563596

564597
result := "";
565598
n := Length(GV_MapNames(attrs));
566599
keys := SSortedList(GV_MapNames(attrs));
567600

601+
# helper for formatting attribute kv pairs
602+
format := function(format, key, val)
603+
tmp := Chomp(val);
604+
if "label" = key and StartsWith(tmp, "<<") and EndsWith(tmp, ">>") then
605+
val := StringFormatted("{}", val);
606+
else
607+
if ' ' in key then
608+
key := StringFormatted("\"{}\"", key);
609+
fi;
610+
611+
if ' ' in val or '>' in val or '^' in val or '#' in val then
612+
val := StringFormatted("\"{}\"", val);
613+
fi;
614+
fi;
615+
616+
return StringFormatted(format, key, val);
617+
end;
618+
568619
if n <> 0 then
569620
Append(result, " [");
570621
for i in [1 .. n - 1] do
571622
key := keys[i];
572623
val := attrs[key];
573624

574-
tmp := Chomp(val);
575-
if "label" = key and StartsWith(tmp, "<<") and EndsWith(tmp, ">>") then
576-
val := StringFormatted("{}", val);
577-
else
578-
# TODO it doesn't seem to be possible to enter the if-statement
579-
# below, even with examples where the key contains spaces (probably
580-
# the quotes are added somewhere else). Either uncomment or delete
581-
# this code.
582-
# if ' ' in key then
583-
# key := StringFormatted("\"{}\"", key);
584-
# fi;
585-
if ' ' in val or '>' in val or '^' in val or '#' in val then
586-
# TODO avoid code duplication here, and below
587-
val := StringFormatted("\"{}\"", val);
588-
fi;
589-
fi;
590-
591-
Append(result,
592-
StringFormatted("{}={}, ",
593-
key,
594-
val));
625+
Append(result, format("{}={}, ", key, val));
595626
od;
596-
597627
# handle last element
598628
key := keys[n];
599629
val := attrs[key];
600-
601-
tmp := Chomp(val);
602-
if "label" = key and StartsWith(tmp, "<<") and EndsWith(tmp, ">>") then
603-
val := StringFormatted("{}", val);
604-
else
605-
if ' ' in key then
606-
key := StringFormatted("\"{}\"", key);
607-
fi;
608-
if ' ' in val or '>' in val or '^' in val or '#' in val then
609-
# TODO what are the allowed things in the value?
610-
val := StringFormatted("\"{}\"", val);
611-
fi;
612-
fi;
613-
614-
Append(result,
615-
StringFormatted("{}={}]",
616-
key,
617-
val));
630+
Append(result, format("{}={}]", key, val));
618631
fi;
619632

620633
return result;
@@ -669,11 +682,9 @@ function(graph, is_subgraph)
669682
elif IsGraphvizGraph(graph) then
670683
Append(result, "//dot\n");
671684
Append(result, GV_StringifyGraphHead(graph));
672-
# TODO doesn't seem to be possible to reach the case below either, uncomment
673-
# or delete
674-
# else
675-
# Append(result, "//dot\n");
676-
# Append(result, GV_StringifyContextHead(graph));
685+
else
686+
ErrorFormatted("Unknown graph category, ",
687+
"expected a context, digraph or graph.");
677688
fi;
678689

679690
Append(result, GV_StringifyGraphAttrs(graph));
@@ -737,3 +748,38 @@ function(gv, colors)
737748
fi;
738749
Perform(colors, ErrorIfNotValidColor);
739750
end);
751+
752+
InstallGlobalFunction(GV_ErrorIfNotValidLabel,
753+
function(label)
754+
local cond;
755+
756+
if Length(label) = 0 then
757+
ErrorFormatted("invalid label \"{}\", valid DOT labels ",
758+
"cannot be empty strings", label);
759+
fi;
760+
761+
# double quoted string
762+
if StartsWith(label, "\"") and EndsWith(label, "\"")then
763+
return;
764+
fi;
765+
# HTML string
766+
if StartsWith(label, "<") and EndsWith(label, ">")then
767+
return;
768+
fi;
769+
770+
# numeral
771+
if Int(label) <> fail then
772+
return;
773+
fi;
774+
775+
cond := not IsDigitChar(label[1]);
776+
cond := cond and ForAll(label, c -> IsAlphaChar(c) or IsDigitChar(c)
777+
or c = '_' or ('\200' <= c and c <= '\377'));
778+
if cond then
779+
return;
780+
fi;
781+
782+
ErrorFormatted("invalid label \"{}\", valid DOT labels ",
783+
"https://graphviz.org/doc/info/lang.html",
784+
label);
785+
end);

tst/dot.tst

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ gap> GraphvizAttrs(g);
2929
[ "color=red", "shape=circle" ]
3030
gap> GraphvizSetAttrs(g, rec(color := "blue", label := "test"));;
3131
gap> GraphvizAttrs(g);
32-
[ "color=red", "shape=circle", "label=test", "color=blue" ]
32+
[ "shape=circle", "label=test", "color=blue" ]
3333

3434
# Test stringify
3535
gap> g := GraphvizGraph();;

0 commit comments

Comments
 (0)