背景
最近,我的同事向我们的测试项目添加了一些新测试。其中之一还没有传递或持续集成系统。由于我们有大约 800 个测试,并且需要一个小时才能运行所有测试,因此我们经常会犯错误,并且只在我们的开发机器上运行我们当前已实现的测试。这种方法有其弱点,因为有时测试在本地通过,但在集成系统上失败。当然,有人可能会说“这不是一个错误,测试应该相互独立!”。
在理想的世界里......当然,但不是在我的世界里。不是在一个有很多单例初始化的世界中initialization
部分,Delphi 本身引入的许多全局变量,在后台初始化的 OTL 线程池,连接到控件以用于绘画目的的 DevExpress 方法..以及许多其他我不知道的事情。因此,在最终结果中,一项测试可以改变其他测试的行为。 (这当然是不好的,我很高兴它发生,因为希望我能够修复另一个依赖项)。
我已经在我的机器上启动了整个测试包,并且获得了与集成系统相同的结果。到目前为止一切顺利,现在我开始关闭测试,直到缩小了干扰最近添加的测试的范围。他们没有任何共同点。我进行了更深入的挖掘,并将问题缩小到一行。如果我评论它 - 测试通过,如果没有 - 测试失败。
Problem
我们有这样的代码将文本数据转换为经度坐标(仅包括重要部分):
procedure TTerminalNVCParserTest_Unit.TranslateGPS_ValidGPSString_ReturnsValidCoords;
const
CStrGPS = 'N5145.37936E01511.8029';
var
LLatitude, LLongitude: Integer;
LLong: Double;
LStrLong, LTmpStr: String;
LFS: TFormatSettings;
begin
FillChar(LFS, SizeOf(LFS), 0);
LFS.DecimalSeparator := '.';
LStrLong := Copy(CStrGPS, Pos('E', CStrGPS)+1, 10);
LTmpStr := Copy(LStrLong,1,3);
LLong := StrToFloatDef( LTmpStr, 0, LFS );
LTmpStr := Copy(LStrLong,4,10);
LLong := LLong + StrToFloatDef( LTmpStr, 0, LFS)*1/60;
LLongitude := Round(LLong * 100000);
CheckEquals(1519671, LLongitude);
end;
问题是LLongitude
有时等于 1519671,有时给出 1519672。并且是否给出 1519672 取决于不同测试中不同方法中其他完全不相关的代码段:
FormXtrMainImport.JvWizard1.SelectNextPage;
我已经四次检查了 SelectNextPage 方法,它不会触发任何可能改变 FPU 单元工作方式的事件。它不会改变的值RoundingMode
它始终设置在 rmNearest 上。
此外,Delphi 不应该在这里使用银行家规则吗? :
LLongitude := Round(LLong * 100000); //LLong * 100000 = 1519671,5
如果使用银行家规则,它应该总是给我 1519672 而不是 1519671。
我猜一定有一些损坏的内存导致了问题以及与SelectNextPage
只揭示它。然而,同样的问题出现在三台不同的机器上。
任何人都可以告诉我如何追踪这个问题吗?或者说如何保证始终稳定的转换结果?
对于那些误解我的问题的人
我已经检查了 RoundingMode 并且之前已经提到过:“我已经四重检查了 SelectNextPage 方法,它不会触发任何可能改变 FPU 单元工作方式的事件。它不会更改 RoundingMode 的值,它始终在 rmNearest 上设置。” 在上述代码中发生任何运行之前,RoundingMode 始终为 rmNearest。
这不是真正的测试。这只是显示问题发生位置的代码。
添加了视频说明。
因此,为了努力改进我的问题,我决定添加显示我的奇怪问题的视频。这是生产代码,我只添加了用于检查 RoundingMode 的断言。
在视频的第一部分中,我展示了原始测试(@Sir Rufo、@Craig Young)、负责转换的方法以及我得到的正确结果。在第二部分中,我展示了当我添加另一个不相关的测试时,我得到了不正确的结果。视频可以找到here
添加了可重现的示例
这一切都归结为以下代码:
procedure FloatingPointNumberHorror;
const
CStrGPS = 'N5145.37936E01511.8029';
var
LLongitude: Integer;
LFloatLon: Double;
adcConnection: TADOConnection;
qrySelect: TADOQuery;
LCSVStringList: TStringList;
begin
//Tested on Delphi 2007, 2009, XE 5 - Windows 7 64 bit
adcConnection := TADOConnection.Create(nil);
qrySelect := TADOQuery.Create(adcConnection);
LCSVStringList := TStringList.Create;
try
//Prepare on the fly csv file required by ADOQuery
LCSVStringList.Add('Col1;Col2;');
LCSVStringList.Add('aaaa;1234;');
LCSVStringList.SaveToFile(ExtractFilePath(ParamStr(0)) + 'test.csv');
qrySelect.CursorType := ctStatic;
qrySelect.Connection := adcConnection;
adcConnection.ConnectionString := 'Provider=Microsoft.Jet.OLEDB.4.0;Data Source='
+ ExtractFilePath(ParamStr(0)) + ';Extended Properties="text;HDR=yes;FMT=Delimited(;)"';
// Real stuff begins here, above we have only preparation of environment.
LFloatLon := 15 + 11.8029*1/60;
LLongitude := Round(LFloatLon * 100000);
Assert(LLongitude = 1519671, 'Asertion 1'); //Here you will NOT receive error.
//This line changes the FPU control word from $1372 to $1272.
//This causes the change of Precision Control Field (PC) from 3 which means
//64bit precision to 2 which means 53 bit precision thus resulting in improper rounding?
//--> ADODB.TParameters.InternalRefresh->RefreshFromOleDB -> CommandPrepare.Prepare(0)
qrySelect.SQL.Text := 'select * from [test.csv] WHERE 1=1';
LFloatLon := 15 + 11.8029*1/60;
LLongitude := Round(LFloatLon * 100000);
Assert(LLongitude = 1519671, 'Asertion 2'); //Here you will receive error.
finally
adcConnection.Free;
LCSVStringList.Free;
end;
end;
只需复制并粘贴此过程并添加ADODB
to use 子句。该问题似乎是由 Delphi 的 ADO 包装器使用的某些 Microsoft COM 对象引起的。该对象正在更改 FPU 控制字,但它是不改变舍入模式。它正在改变精确控制。
以下是启动 ADO 相关方法之前和之后的 FPU 屏幕截图:
![FPU screenshot](https://i.stack.imgur.com/KUAET.png)
我想到的唯一解决方案是使用Get8087CW
在使用 ADO 代码之前然后Set8087CW
使用先前存储的控制世界来设置控制世界。