Background
Recently, my colleague added several new tests to our test project. One of them failed or did not integrate the integration system. Since we have about 800 tests, and it takes an hour to start all of this, we often make mistakes and run on our machines only the tests that we have currently implemented. This method has its weakness, because from time to time the tests pass locally, and then refuse to integrate. Of course, someone could say: "This is not a mistake, the tests must be independent of each other!".
In an ideal world ... of course, but not in my world. Not in a world in which you have many loners initialized in the initialization section, many global variables entered by Delphi itself, in OTL the thread pool is initialized in the background, DevExpress methods are connected to controls for painting purposes .. and dozens of other things. which I donβt know about. Thus, in the end result, one test can change the behavior of another test. (This, of course, is bad, and I am glad that this will happen, because, I hope, I can fix another dependency).
I started the entire test package on my machine, and I achieved the same results as in the integration system. So far so good, now I started to turn off the tests, until I narrowed down one test that interfered with the recently added one. They have nothing in common. I went deeper and narrowed the problem down to one line. If I comment on this - test passes, if not - the test fails.
Problem
We have such code for converting text data to longitude coordinates (only the important part was included):
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;
The problem is that LLongitude sometimes equal to 1519671, and sometimes it gives 1519672. And whether it gives 1519672 or not depends on another completely unrelated part of the code in another method in another test:
FormXtrMainImport.JvWizard1.SelectNextPage;
I checked the four-local SelectNextPage method, it does not fire any event that can change the operation of the FPU module. It does not change the value of RoundingMode , it is always set to rmNearest.
Also, shouldn't Delphi use the banker rule here?
LLongitude := Round(LLong * 100000); //LLong * 100000 = 1519671,5
If a banker's rule is used, it should always give me 1519672 not 1519671.
I assume that there should be some corrupted memory that causes the problem, and the line with SelectNextPage shows it only. However, the same problem occurs on three different machines.
Can anyone give me an idea on how to trace this issue? Or how to always provide a stable conversion result?
Those who misunderstood my question
I checked RoundingMode, and I mentioned it before: "I checked the four-local SelectNextPage method, it does not fire any event that can change the operation of the FPU. The value of RoundingMode is always set to rmNearest." RoundingMode is always rmNearest before any runding happens in the code above.
This is not a real test. This is just code to show where the problem is.
Video description added.
So, trying to improve my question, I decided to add a video that shows my problem with bizzare. This is production code, I just added assertions to test RoundingMode. In the first part of the video, I show the original test (@Sir Rufo, @Craig Young), the method responsible for the conversion, and the correct result that I get. In the second part, I show that when I add another unrelated test, I get the wrong result. The video can be found here.
Added example of reproducible
It all comes down to the code below:
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;
Just copy and paste this procedure and add the ADODB clause to uses. It seems that the problem is caused by some Microsoft COM object used by the Delphi ADO shell. This object changes the control word FPU, but it does not change the rounding mode. It changes precision control.
Here is a screenshot of the FPU before and after activating the ADO-related method:

The only solution that comes to my mind is to use Get8087CW before using the ADO code, and then Set8087CW to set up a control world with a previously saved one.