在下面的评论中详细说明讨论 my last question :我正在寻找有关构建 SWI-Prolog 代码的技术或最佳实践的建议,以便能够使用和测试算法及其支持模块的替代、可互换实现。
当前情况可以使用以下虚构的小示例来说明:用户提供一些输入数据(文件 data.pl
)并加载一个带有要应用的算法的模块(文件 graph.pl
)。算法模块本身使用来自另一个模块(文件 path.pl
)的辅助谓词,而后者又需要访问用户提供的数据:
文件' data.pl
'(输入数据集):
:- use_module(graph).
edge(a,b).
edge(b,c).
edge(c,d).
文件'
graph.pl
' (算法)::- module(graph, [reachable/2]).
:- use_module(path).
reachable(X,Y) :-
path(X,Y), !.
reachable(X,Y) :-
path(Y,X), !.
文件'
path.pl
'(带有辅助谓词的模块,注意它访问 user
中的数据)::- module(path, [path/2]).
path(X,X).
path(X,Y) :-
user:edge(X,Z),
path(Z,Y).
对于将算法应用于单个输入数据集和算法的单个实现的用例,这完全没问题:
?- [data].
true.
?- reachable(a,a).
true.
?- reachable(a,d).
true.
?- reachable(d,a).
true.
现在假设我有大量的数据集,以及
graph
的多个替代实现。和 path
模块(具有相同的接口(interface),即导出的谓词)。为了(小)示例,让我们假设我们归档数据文件 data1.pl
, data2.pl
, 辅助谓词模块 path1.pl
, path2.pl
, 和算法模块 graph1
, graph2.pl
.我想使用 SWI-Prolog unit tests 自动测试这些,并且最好能够编写支持不同数据集和不同模块实现的测试套件,而无需在两者之间重新启动 Prolog。也就是说我希望能够测试笛卡尔积中的所有组合
{data1.pl, data2.pl} x {path1.pl, path2.pl} x {graph1.pl, graph2.pl}
没有复制粘贴/复制代码。
我的问题是:我将如何在 SWI-Prolog 中解决这个问题?是否有关于如何为此目的将代码组织成模块的最佳实践、设计模式等?我应该使用 dynamic importing用于在替代算法模块之间切换,只需使用
setup
和 cleanup
在数据的单元测试中?
最佳答案
为了将同一组测试应用于相同谓词的不同实现,或者更一般地说,应用于同一接口(interface)/协议(protocol)的不同实现,测试必须将实现作为动态参数。理想情况下,我们还应该能够使用不同的数据集测试不同的算法实现。
一个单独的问题是如何组织数据以及我们想要在数据上运行的算法。有两种明智的做法。第一个选项是将数据视为导入或继承算法实现。在这种情况下,查询(例如 reachable/2
)将被发送到数据。该解决方案的一个缺点是,每次我们想要应用不同的算法集(例如,通过导入不同的模块)时,我们可能需要更新数据集。
第二种选择是将数据视为算法的参数。此解决方案的一个简单实现是向谓词(例如路径和可达谓词)添加一个额外的参数,用于传递对数据的引用(例如问题中提到的简单情况下的 user
)。这个解决方案的一个缺点是所有与算法相关的谓词都需要额外的参数(例如 reachable/2
只调用 path/2
并且只有这个谓词实际调用 edge/2
)。
所有上述问题和相应的替代解决方案都可以使用 Logtalk 参数对象而不是 Prolog 模块和使用 Logtalk 单元测试框架轻松清晰地表达,lgtunit
,它支持开箱即用的参数化测试。遵循示例解决方案(它是可移植的,可以与大多数 Prolog 系统一起使用)。
首先,让我们只制作关于数据的数据。我们首先为所有数据对象定义一个协议(protocol)/接口(interface):
:- protocol(graph_protocol).
:- public(edge/2).
...
:- end_protocol.
所有数据对象都将实现该协议(protocol)。例如:
:- object(graph1,
implements(graph_protocol)).
edge(a,b).
edge(b,c).
edge(c,d).
:- end_object.
接下来,让我们定义参数对象,其中包含传递数据集对象的单个参数的算法。这些对象可能还会实现定义的协议(protocol),指定我们希望为其提供替代实现的谓词。为简洁起见,这里省略了这些协议(protocol)。
:- object(path(_Data_)).
:- uses(_Data_, [edge/2]).
:- public(path/2).
path(X,X).
path(X,Y) :-
edge(X,Z),
path(Z,Y).
:- end_object.
:- object(reachable(_Data_)).
:- uses(path(_Data_), [path/2]).
:- public(reachable/2).
reachable(X,Y) :-
path(X,Y), !.
reachable(X,Y) :-
path(Y,X), !.
:- end_object.
请注意,这些对象按原样使用您的谓词定义(
uses/2
对象中的 reachable/1
指令需要 Logtalk 3.28.0 或更高版本)。数据集加载到
user
中的默认情况可以通过定义来简化::- object(reachable ,
extends(reachable(user))).
:- end_object.
一个典型的查询是:
?- reachable(graph1)::reachable(a,d).
...
到目前为止,我们只是参数化数据集,而不是算法。我们会到达那里。测试也可以定义为参数对象。例如:
:- object(tests(_Data_),
extends(lgtunit)).
:- uses(reachable(_Data_), [reachable/2]).
test(r1) :-
reachable(a,a).
test(r2) :-
reachable(a,d).
test(r3) :-
reachable(d,a).
:- end_object.
对多个数据集进行测试将使用一个目标,例如:
lgtunit::run_test_sets([
tests(graph1),
tests(graph2),
tests(graph3)
])
最初的问题侧重于测试替代的、可互换的算法实现。但解决方法是一样的。我们只需要修改参数测试对象,以将实现被测试算法的对象作为参数:
:- object(tests(_Algorithm_),
extends(lgtunit)).
:- uses(_Algorithm_, [reachable/2]).
cover(reachable(_)).
cover(path(_)).
test(r1) :-
reachable(a,a).
test(r2) :-
reachable(a,d).
test(r3) :-
reachable(d,a).
:- end_object.
然后,在运行测试的查询中,使用我们想要的任何数据集和算法组合。例如:
lgtunit::run_test_sets([
tests(reachable1(graph1)), tests(reachable2(graph1)),
tests(reachable1(graph2)), tests(reachable2(graph2)),
...
])
lgtunit::run_test_sets/1
的列表参数谓词也可以动态创建。例如,假设 reachable/2
的所有替代实现谓词实现一个 reachable_protocol
协议(protocol),测试目标可能是:datasets(Datasets),
findall(
tests(Algorithm),
( implements_protocol(Algorithm, reachable_protocol),
member(Dataset, Datasets),
arg(1, Algorithm, Dataset)
),
TestObjects
),
lgtunit::run_test_sets(TestObjects)
使用
lgtunit
运行这些测试的一个值得注意的方面也就是说,除了报告通过和失败的测试之外,在谓词子句级别报告完整的谓词代码覆盖率也很简单。这意味着我们不仅要测试算法,还要检查用于实现算法的所有子句是否都被使用。对于此示例,仅使用 graph1
数据集,顶层解释器的测试输出为:?- {tester}.
%
% tests started at 2019/8/5, 7:17:46
%
% running tests from object tests(graph1)
% file: /Users/pmoura/Desktop/plu/tests.lgt
%
% g1: success
% g2: success
% g3: success
%
% 3 tests: 0 skipped, 3 passed, 0 failed
% completed tests from object tests(graph1)
%
%
% clause coverage ratio and covered clauses per entity predicate
%
% path(A): path/2 - 2/2 - (all)
% path(A): 2 out of 2 clauses covered, 100.000000% coverage
%
% reachable(A): reachable/2 - 2/2 - (all)
% reachable(A): 2 out of 2 clauses covered, 100.000000% coverage
%
% 2 entities declared as covered containing 4 clauses
% 2 out of 2 entities covered, 100.000000% entity coverage
% 4 out of 4 clauses covered, 100.000000% clause coverage
%
% tests ended at 2019/8/5, 7:17:46
%
true.
如果您正在自动化测试(例如使用 CI 服务器),您可以使用
logtalk_tester
代替脚本。如果我们想继续使用数据集和/或算法的模块怎么办?对于测试对象,这只是一个编写问题:
:- object(tests(_Algorithm_),
extends(lgtunit)).
:- use_module(_Algorithm_, [reachable/2]).
...
:- end_object.
Logtalk的
lgtunit
支持测试普通 Prolog 代码和 Prolog 模块代码,除了 Logtalk 代码(实际上,Logtalk 发行版包括 Prolog 标准一致性测试套件)。有关工具概述,请参见例如https://logtalk.org/tools.html#testing
在上面的 URL 中,我们还会找到一个代码覆盖率报告示例。有关使用上述解决方案的完整代码示例,请参见例如
https://github.com/LogtalkDotOrg/logtalk3/tree/master/library/dictionaries
该库提供了字典 API 的三种替代实现和一组测试(使用参数对象)来测试所有这些。
最后但并非最不重要的是,您不仅可以将此测试解决方案与 SWI-Prolog 一起使用,还可以将其与 10 个其他 Prolog 系统一起使用。
关于unit-testing - 将 SWI-Prolog 代码构建到模块中,用于多个算法和数据集的单元测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57341339/