背景
最近,我的同事在我们的测试项目中添加了一些新的测试.其中一个没有传递或持续集成系统.由于我们有大约800个测试,并且运行所有这些测试需要一个小时,因此我们经常会犯错误并在我们的开发机器上运行我们当前实施的测试.这种方法有其弱点,因为有时测试是在本地传递但在集成系统上失败.当然,有人可以说"这不是一个错误,测试应该是彼此独立的!".
在理想的世界......当然,但不是在我的世界.不是在你有很多单例初始化的世界里,initialization
Delphi本身引入了很多全局变量,在后台初始化了一个OTL线程池,DevExpress方法挂钩你的控件用于绘画目的......还有很多其他的东西我不知道.因此,在最终结果中,一个测试可以改变其他测试的行为.(当然这本身就很糟糕,我很高兴它会发生,因为希望我能够修复另一个依赖).
我已经在我的机器上启动了整个测试包,并且我已经获得了与集成系统相同的结果.到目前为止一直很好,现在我开始关闭测试,直到我缩小了一个干扰最近添加的测试的测试.他们没有任何共同之处.我已经深入挖掘,并将问题缩小到一条线.如果我评论它 - 测试通过,如果没有 - 测试失败.
问题
我们有这样的代码将文本数据转换为经度坐标(仅包括重要部分):
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上设置.
此外,德尔福不应该在这里使用银行家规则吗?:
LLongitude := Round(LLong * 100000); //LLong * 100000 = 1519671,5
如果使用银行家规则,它应该给我总是1519672而不是1519671.
我想必须有一些损坏的内存导致问题,而线路SelectNextPage
只显示它.但是在三台不同的机器上会出现同样的问题.
任何人都可以告诉我如何追踪这个问题?或者如何确保始终获得稳定的转换结果?
对那些误解我的问题的人
我已经检查了RoundingMode并且我之前提到过它:"我已经检查了SelectNextPage方法的四倍,它不会触发任何可能改变FPU单元工作方式的事件.它不会改变RoundingMode的值它总是在rmNearest上建立." 在上述代码中出现任何runding之前,RoundingMode始终是rmNearest.
这不是真正的考验.这只是显示问题发生位置的代码.
视频说明已添加.
因此,在努力改进我的问题时,我决定添加显示我的眩晕问题的视频.这是生产代码,我只添加断言来检查RoundingMode.在视频的第一部分,我将展示原始测试(@Sir Rufo,@ Craig Young),负责转换的方法以及我得到的正确结果.在第二部分中,我将展示当我添加另一个不相关的测试时,我得到的结果不正确.视频可以在这里找到
添加了可重复的示例
这一切归结为以下代码:
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
到uses子句即可.似乎问题是由Delphi的ADO包装器使用的某些Microsoft COM对象引起的.此对象正在更改FPU控制字,但它不会更改舍入模式.它正在改变精确控制.
这是启动ADO相关方法之前和之后的FPU屏幕截图:
我想到的唯一解决方案是Get8087CW
在使用ADO代码之前使用,然后Set8087CW
使用先前存储的代码设置控制世界.