为了在下面的我的最后一个问题注释中详细讨论:我正在寻找关于构造supporting代码的技术或最佳实践的建议,以便能够使用和测试算法及其支持模块的可替换的、可替换的实现。
当前的情况可以用以下小的、虚构的例子来说明:用户提供一些输入数据(文件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
模块的多个替代实现(具有相同的接口,即导出谓词)。为了(小)的例子,让我们假设我们文件数据文件data1.pl
,data2.pl
,助手谓词模块path1.pl
,path2.pl
,和算法模块graph1
,graph2.pl
.。
我希望使用SWI-Prolog单元测试自动测试这些数据,并且最好能够编写一个测试套件,它既支持不同的数据集,又支持不同的模块实现,而不需要在两者之间重新启动Prolog。也就是说,我希望能够测试笛卡儿积中的所有组合。
{data1.pl, data2.pl} x {path1.pl, path2.pl} x {graph1.pl, graph2.pl}
没有复制粘贴/复制代码。
我的问题是:我将如何在SWI中做到这一点?关于如何为此目的将代码构造成模块,是否有最佳实践、设计模式等?我是否应该使用动态进口在替代算法模块之间切换,并在数据的单元测试中简单地使用setup
和cleanup
?
发布于 2019-08-03 23:31:48
首先,您有元谓词。这些应该允许您将数据和算法的构建块作为参数传递。看看这个例子。在绝对确定这种方法不够好之前,我不会尝试任何更复杂的方法。
最后,您始终可以回到手动管理数据库的断言、撤回、取消等等。如果这样做,您可以完全避免模块系统。
但是试着先用元谓词来做。这是Prolog中“泛型算法”的明显机制。
一些密码。首先,您可以使用单元测试盒做什么?你可以做以下几件事。以下是三个模块:
$ cat foo.pl
:- module(foo, [x/1]).
x(foo).
$ cat bar.pl
:- module(bar, [x/1]).
x(bar).
$ cat baz.pl
:- module(baz, []).
:- begin_tests(foo).
:- use_module(foo).
test(x) :- x(foo).
:- end_tests(foo).
:- begin_tests(bar).
:- use_module(bar).
test(x) :- x(bar).
:- end_tests(bar).
最后一个模块baz
还没有导出任何东西,但是它确实有两个独立的单元测试盒。加载模块并运行测试:
$ swipl
Welcome to SWI-Prolog (threaded, 64 bits, version 8.1.10-59-g09a7d554d-DIRTY)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.
For online help and background, visit http://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).
?- use_module(baz).
true.
?- run_tests.
% PL-Unit: foo . done
% PL-Unit: bar . done
% All 2 tests passed
true.
显然单元文本框可以让你有作用域。
要澄清的是,您可以在没有元调用(所以没有附加参数)的情况下使用客户机代码来假设接口(在本例中是对x/1
的调用)。然后,您可以通过在同一个文件中的两个独立单元测试框中导入两个相互竞争的模块来测试相同接口的不同实现。
无论如何,所有这些似乎都可以用Logtalk更干净的方式完成。
发布于 2019-08-04 06:36:47
为了将相同的测试集应用于相同谓词的不同实现,或者更笼统地说,应用于相同接口/协议的不同实现,测试必须将实现作为动态参数。理想情况下,我们还应该能够用不同的数据集测试不同的算法实现。
另一个关注的问题是如何组织数据和我们想要在数据上运行的算法。有两种明智的方法。第一个选项是将数据视为导入或继承算法实现。在这种情况下,查询(例如reachable/2
)将发送到数据。这种解决方案的一个缺点是,每当我们想要应用不同的算法集(例如,通过导入不同的模块)时,我们都可能需要更新数据集。
第二个选项是将数据视为算法的一个参数。该解决方案的一个简单实现是在谓词(例如路径和可访问谓词)中添加一个额外的参数,用于传递对数据的引用(例如,问题中提到的简单情况下的user
)。该解决方案的一个缺点是,所有与算法相关的谓词都需要额外的参数(例如,reachable/2
只调用path/2
,并且只有这个实际上调用edge/2
的谓词)。
以上所有问题和相应的备选解决方案都可以用Logtalk参数对象代替Prolog模块和使用Logtalk单元测试框架lgtunit
(支持参数化测试的开箱即用)轻松而清晰地表达出来。下面是一个示例解决方案(它是可移植的,可以用于大多数Prolog系统)。
首先,让我们只制作关于数据的数据。我们首先为所有数据对象定义一个协议/接口:
:- protocol(graph_protocol).
:- public(edge/2).
...
:- end_protocol.
所有数据对象都将实现此协议。例如:
:- object(graph1,
implements(graph_protocol)).
edge(a,b).
edge(b,c).
edge(c,d).
:- end_object.
接下来,让我们定义包含算法的参数对象,单个参数就是传递dataset对象。这些对象还可能实现已定义的协议,指定我们希望为其提供替代实现的谓词。为了简洁起见,这里省略了这些协议。
:- 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.
注意,这些对象使用谓词定义为-is( reachable/1
对象中的reachable/1
指令需要LogTalk3.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
谓词的list参数。例如,假设reachable/2
谓词的所有替代实现都实现了reachable_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
脚本。
如果我们想继续为数据集和/或算法使用模块怎么办?对于test对象来说,它只是一个编写的问题:
:- 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,而且还+10其他Prolog系统。
发布于 2019-08-03 12:27:40
对于单元测试,绝对使用setup/1
和cleanup/1
,您希望测试用例。
为了您自己的探索和灵活性,重新定义依赖树,您不希望用用户命名空间调用谓词,因为当您的导入变得更加复杂或移动时,它将无法工作。该算法依赖于实用程序谓词,然后该谓词需要它所操作的数据。
文件'data.pl‘(输入数据集):
:- module(data, [edge/2]).
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‘(带有助手谓词的模块,注意它访问使用模块中的数据):
:- module(path, [path/2]).
:- use_module(data).
path(X,X).
path(X,Y) :-
edge(X,Z),
path(Z,Y).
现在你可以swipl -g "reachable(a, d)" -s graph.pl
了。这将使您可以轻松地更改path.pl
中使用的数据模块。如果您愿意,可以在这里使用谓词动态加载模块,但最好在单元测试中使用安装/清理:
:- dynamic path:edge/2.
/* Testing Graph
a→b→c→d
*/
setup :-
asserta(path:edge(a,b)),
asserta(path:edge(b,c)),
asserta(path:edge(c,d)).
cleanup :-
retractall(path:edge(_, _)).
test(reach_same,
[ true(A, a)
, setup(setup)
, cleanup(cleanup)
, nondet
]
) :-
reachable(a, A).
https://stackoverflow.com/questions/57341339
复制相似问题