diff --git a/CHANGELOG.md b/CHANGELOG.md index 1844df7..849816b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.3.0](https://github.com/appercept/Delphi-WebMocks/tree/1.3.0) (2020-10-07) + +[Full Changelog](https://github.com/appercept/Delphi-WebMocks/compare/1.2.2...1.3.0) + +**Implemented enhancements:** + +- Add auto-port allocation [\#31](https://github.com/appercept/Delphi-WebMocks/pull/31) ([rhatherall](https://github.com/rhatherall)) + +**Closed issues:** + +- In large projects you can frequently hit port clashes [\#30](https://github.com/appercept/Delphi-WebMocks/issues/30) + ## [1.2.2](https://github.com/appercept/Delphi-WebMocks/tree/1.2.2) (2020-07-25) [Full Changelog](https://github.com/appercept/Delphi-WebMocks/compare/1.2.1...1.2.2) diff --git a/README.md b/README.md index 4600be0..1d34188 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ WebMocks should now be listed in ## Installation: Manual 1. Download and extract the latest version - [1.2.2](https://github.com/appercept/Delphi-WebMocks/archive/1.2.2.zip). + [1.3.0](https://github.com/appercept/Delphi-WebMocks/archive/1.3.0.zip). 2. Open the package appropriate for your Delphi version in the `Packages` folder. 3. Build and install the package. @@ -97,15 +97,15 @@ initialization end. ``` -By default `TWebMock` will bind to port `8080`. The port can be specified at -creation. +By default `TWebMock` will bind to a port dynamically assigned start at `8080`. +This behaviour can be overriden by sepcifying a port at creation. ```Delphi WebMock := TWebMock.Create(8088); ``` The use of `WebMock.URLFor` function within your tests is to simplify -constructing a valid URL. The `BaseURL` property contains a valid URL for the -server root. +constructing a valid URL. The `Port` property containes the current bound port +and `BaseURL` property contains a valid URL for the server root. ## Examples ### Stubbing diff --git a/Source/Delphi.WebMock.pas b/Source/Delphi.WebMock.pas index ca7e9b0..276daec 100644 --- a/Source/Delphi.WebMock.pas +++ b/Source/Delphi.WebMock.pas @@ -33,18 +33,24 @@ interface Delphi.WebMock.Dynamic.RequestStub, Delphi.WebMock.Response, Delphi.WebMock.ResponseBodySource, Delphi.WebMock.ResponseStatus, IdContext, IdCustomHTTPServer, IdGlobal, IdHTTPServer, - System.Classes, System.Generics.Collections, System.RegularExpressions; + System.Classes, System.Generics.Collections, System.RegularExpressions, + System.SysUtils; type + EWebMockError = class(Exception); + EWebMockExceededBindAttempts = class(EWebMockError); + TWebWockPort = TIdPort; TWebMock = class(TObject) + class var NextPort: Integer; private FServer: TIdHTTPServer; FBaseURL: string; FStubRegistry: TList; FHistory: TList; procedure InitializeServer(const APort: TWebWockPort); + procedure StartServer(const APort: TWebWockPort); procedure OnServerRequest(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo); function GetRequestStub(ARequestInfo: IWebMockHTTPRequest) : IWebMockRequestStub; @@ -56,9 +62,11 @@ TWebMock = class(TObject) const AResponseHeaders: TStrings); procedure SetResponseStatus(AResponseInfo: TIdHTTPResponseInfo; const AResponseStatus: TWebMockResponseStatus); + function GetNextPort: Integer; + function GetPort: Integer; property Server: TIdHTTPServer read FServer write FServer; public - constructor Create(const APort: TWebWockPort = 8080); + constructor Create(const APort: TWebWockPort = 0); destructor Destroy; override; function Assert: TWebMockAssertion; procedure PrintStubRegistry; @@ -75,6 +83,7 @@ TWebMock = class(TObject) property BaseURL: string read FBaseURL; property History: TList read FHistory; property StubRegistry: TList read FStubRegistry; + property Port: Integer read GetPort; end; implementation @@ -82,9 +91,10 @@ implementation uses Delphi.WebMock.HTTP.Request, Delphi.WebMock.HTTP.RequestMatcher, + IdException, IdHTTP, IdSocketHandle, - System.SysUtils; + IdStack; { TWebMock } @@ -93,7 +103,7 @@ function TWebMock.Assert: TWebMockAssertion; Result := TWebMockAssertion.Create(History); end; -constructor TWebMock.Create(const APort: TWebWockPort = 8080); +constructor TWebMock.Create(const APort: TWebWockPort = 0); begin inherited Create; FStubRegistry := TList.Create; @@ -109,6 +119,22 @@ destructor TWebMock.Destroy; inherited; end; +function TWebMock.GetNextPort: Integer; +var + FIsInitial: Boolean; +begin + AtomicCmpExchange(NextPort, 8080, 0, FIsInitial); + if FIsInitial then + Exit(NextPort); + + Result := AtomicIncrement(NextPort); +end; + +function TWebMock.GetPort: Integer; +begin + Result := Server.Bindings.Items[0].Port; +end; + function TWebMock.GetRequestStub(ARequestInfo: IWebMockHTTPRequest) : IWebMockRequestStub; var LRequestStub: IWebMockRequestStub; @@ -131,11 +157,9 @@ procedure TWebMock.InitializeServer(const APort: TWebWockPort); FServer := TIdHTTPServer.Create; Server.ServerSoftware := 'Delphi WebMocks'; - Server.DefaultPort := APort; Server.OnCommandGet := OnServerRequest; Server.OnCommandOther := OnServerRequest; - Server.Active := True; - FBaseURL := Format('http://127.0.0.1:%d/', [Server.DefaultPort]); + StartServer(APort); end; procedure TWebMock.OnServerRequest(AContext: TIdContext; @@ -209,6 +233,44 @@ procedure TWebMock.SetResponseStatus(AResponseInfo: TIdHTTPResponseInfo; AResponseInfo.ResponseText := AResponseStatus.Text; end; +procedure TWebMock.StartServer(const APort: TWebWockPort); +var + LAttempt, LMaxAttempts: Integer; + LPort: Integer; + LSocketHandle: TIdSocketHandle; +begin + LAttempt := 0; + LMaxAttempts := 3; + while not Server.Active do + begin + Inc(LAttempt); + if LAttempt >= LMaxAttempts then + raise EWebMockExceededBindAttempts.Create('Exceeded attempts to bind port.'); + if APort > 0 then + LPort := APort + else + LPort := GetNextPort; + Server.Bindings.Clear; + LSocketHandle := Server.Bindings.Add; + LSocketHandle.Port := LPort; + try + Server.Active := True; + FBaseURL := Format('http://127.0.0.1:%d/', [LSocketHandle.Port]); + except + on E: EIdCouldNotBindSocket do + begin + Server.Active := False; + StartServer(APort); + end; + on E: EIdSocketError do + begin + Server.Active := False; + StartServer(APort); + end; + end; + end; +end; + function TWebMock.StubRequest( const AMatcher: TWebMockDynamicRequestMatcher): TWebMockDynamicRequestStub; var diff --git a/Tests/Delphi.WebMock.Tests.pas b/Tests/Delphi.WebMock.Tests.pas index 713e9e9..1cbcc9c 100644 --- a/Tests/Delphi.WebMock.Tests.pas +++ b/Tests/Delphi.WebMock.Tests.pas @@ -45,13 +45,15 @@ TWebMockTests = class(TObject) [TearDown] procedure TearDown; [Test] - procedure Create_WithNoArguments_StartsListeningOnPort8080; + procedure Create_WithNoArguments_StartsListeningOnPortGreaterThan8080; + [Test] + procedure Create_WithNoArgumentsWhenRepeated_StartsListeningOnDifferentPorts; [Test] procedure Create_WithPort_StartsListeningOnPortPort; [Test] - procedure BaseURL_ByDefault_ReturnsLocalHostURLWithDefaultPort; + procedure BaseURL_ByDefault_ReturnsLocalHostURLWithPort; [Test] - procedure BaseURL_WhenPortIsNotDefault_ReturnsLocalHostURLWithPort; + procedure Port_Always_ReturnsTheListeningPort; [Test] procedure Reset_Always_ClearsHistory; [Test] @@ -96,34 +98,36 @@ procedure TWebMockTests.URLFor_GivenEmptyString_ReturnsBaseURL; procedure TWebMockTests.URLFor_GivenStringWithLeadingSlash_ReturnsCorrectlyJoinedURL; begin - Assert.AreEqual('http://127.0.0.1:8080/file', WebMock.URLFor('/file')); + Assert.AreEqual(Format('http://127.0.0.1:%d/file', [WebMock.Port]), WebMock.URLFor('/file')); end; procedure TWebMockTests.URLFor_GivenStringWithoutLeadingSlash_ReturnsCorrectlyJoinedURL; begin - Assert.AreEqual('http://127.0.0.1:8080/file', WebMock.URLFor('file')); + Assert.AreEqual(Format('http://127.0.0.1:%d/file', [WebMock.Port]), WebMock.URLFor('file')); end; -procedure TWebMockTests.BaseURL_ByDefault_ReturnsLocalHostURLWithDefaultPort; +procedure TWebMockTests.BaseURL_ByDefault_ReturnsLocalHostURLWithPort; begin - Assert.AreEqual('http://127.0.0.1:8080/', WebMock.BaseURL); + Assert.AreEqual(Format('http://127.0.0.1:%d/', [WebMock.Port]), WebMock.BaseURL); end; -procedure TWebMockTests.BaseURL_WhenPortIsNotDefault_ReturnsLocalHostURLWithPort; +procedure TWebMockTests.Create_WithNoArgumentsWhenRepeated_StartsListeningOnDifferentPorts; +var + LWebMock1, LWebMock2: TWebMock; begin - WebMock.Free; - WebMock := TWebMock.Create(8088); + LWebMock1 := TWebMock.Create; + LWebMock2 := TWebMock.Create; - Assert.AreEqual('http://127.0.0.1:8088/', WebMock.BaseURL); + Assert.IsTrue(LWebMock2.Port > LWebMock1.Port); end; -procedure TWebMockTests.Create_WithNoArguments_StartsListeningOnPort8080; +procedure TWebMockTests.Create_WithNoArguments_StartsListeningOnPortGreaterThan8080; var LResponse: IHTTPResponse; begin - LResponse := WebClient.Get('http://localhost:8080/'); + LResponse := WebClient.Get(WebMock.URLFor('/')); - Assert.AreEqual('Delphi WebMocks', LResponse.HeaderValue['Server']); + Assert.IsTrue(WebMock.Port > 8080); end; procedure TWebMockTests.Create_WithPort_StartsListeningOnPortPort; @@ -132,12 +136,17 @@ procedure TWebMockTests.Create_WithPort_StartsListeningOnPortPort; begin WebMock.Free; - WebMock := TWebMock.Create(8088); - LResponse := WebClient.Get('http://localhost:8088/'); + WebMock := TWebMock.Create(8079); + LResponse := WebClient.Get('http://localhost:8079/'); Assert.AreEqual('Delphi WebMocks', LResponse.HeaderValue['Server']); end; +procedure TWebMockTests.Port_Always_ReturnsTheListeningPort; +begin + Assert.IsTrue(WebMock.Port > 0); +end; + procedure TWebMockTests.ResetHistory_Always_ClearsHistory; begin WebClient.Get(WebMock.URLFor('history')); diff --git a/Tests/Delphi.WebMocks.Tests.dproj b/Tests/Delphi.WebMocks.Tests.dproj index 48ccc59..a3df349 100644 --- a/Tests/Delphi.WebMocks.Tests.dproj +++ b/Tests/Delphi.WebMocks.Tests.dproj @@ -1,7 +1,7 @@  {04081825-8AE9-4D09-A947-7FE0B62D328A} - 19.0 + 19.1 None Delphi.WebMocks.Tests.dpr True @@ -66,6 +66,7 @@ DBXSqliteDriver;RESTComponents;fmxase;DBXInterBaseDriver;emsclientfiredac;tethering;DataSnapFireDAC;bindcompfmx;fmx;FireDACIBDriver;FireDACDBXDriver;dbexpress;IndyCore;dsnap;emsclient;DataSnapCommon;FireDACCommon;RESTBackendComponents;soapserver;bindengine;CloudService;FireDACCommonDriver;DataSnapClient;inet;IndyIPCommon;bindcompdbx;IndyIPServer;IndySystem;fmxFireDAC;FireDAC;FireDACSqliteDriver;soaprtl;DbxCommonDriver;xmlrtl;soapmidas;DataSnapNativeClient;FireDACDSDriver;rtl;DbxClientDriver;CustomIPTransport;bindcomp;IndyIPClient;dbxcds;dsnapxml;DataSnapProviderClient;dbrtl;IndyProtocols;$(DCC_UsePackage) + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_1024x1024.png DBXSqliteDriver;RESTComponents;fmxase;DBXInterBaseDriver;emsclientfiredac;tethering;DataSnapFireDAC;bindcompfmx;fmx;FireDACIBDriver;FireDACDBXDriver;dbexpress;IndyCore;dsnap;emsclient;DataSnapCommon;FireDACCommon;RESTBackendComponents;soapserver;bindengine;CloudService;FireDACCommonDriver;DataSnapClient;inet;IndyIPCommon;bindcompdbx;IndyIPServer;IndySystem;fmxFireDAC;FireDAC;FireDACSqliteDriver;soaprtl;DbxCommonDriver;xmlrtl;soapmidas;DataSnapNativeClient;FireDACDSDriver;rtl;DbxClientDriver;CustomIPTransport;bindcomp;IndyIPClient;dbxcds;dsnapxml;DataSnapProviderClient;dbrtl;IndyProtocols;$(DCC_UsePackage) @@ -560,6 +561,32 @@ 0 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + 1 @@ -734,6 +761,56 @@ 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + 1 @@ -928,6 +1005,66 @@ 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + 1