Repository: Azure/azure-sdk-for-go
Pull request #17708 (2022-04-22 17:05:14)

Merge arm/runtime/poller.go into runtime/poller.go
LRO refactoring has alleviated the need for separation of ARM and
data-plane pollers so the content has been merged into the runtime
package.  The duplicated content under arm/runtime will be removed in a
future release.
The arm/internal/pollers/loc package was renamed to armloc to avoid
colliding with the existing data-plane package.  These will need to be
consolidated in a separate change.
No functional changes to the relocated, internal, pollers.

<!--
Thank you for contributing to the Azure SDK for Go.

Please verify the following before submitting your PR, thank you!
-->

- [ ] The purpose of this PR is explained in this or a referenced issue.
- [ ] The PR does not update generated files.
   - These files are managed by the codegen framework at [Azure/autorest.go][].
- [ ] Tests are included and/or updated for code changes.
- [ ] Updates to [CHANGELOG.md][] are included.
- [ ] MIT license headers are included in each file.

[Azure/autorest.go]: https://github.com/Azure/autorest.go
[CHANGELOG.md]: https://github.com/Azure/azure-sdk-for-go/blob/main/CHANGELOG.md


jhendrixMSFT: Fixes https://github.com/Azure/azure-sdk-for-go/issues/17698
jhendrixMSFT: I've tested the changes with the autorest test suite in `autorest.go` and everything is working as expected.

Patch:
diff --git a/sdk/azcore/CHANGELOG.md b/sdk/azcore/CHANGELOG.md
index cd90ab58d8..0e87951b90 100644
--- a/sdk/azcore/CHANGELOG.md
+++ b/sdk/azcore/CHANGELOG.md
@@ -13,6 +13,8 @@
 ### Bugs Fixed
 
 ### Other Changes
+* The functionality in `arm/runtime/poller.go` has been merged into `runtime/poller.go` so it should be used instead.
+  * `arm/runtime/poller.go` will be removed in a future release.
 
 ## 0.23.1 (2022-04-14)
 
diff --git a/sdk/azcore/arm/internal/pollers/pollers.go b/sdk/azcore/arm/internal/pollers/pollers.go
deleted file mode 100644
index 3bad751076..0000000000
--- a/sdk/azcore/arm/internal/pollers/pollers.go
+++ /dev/null
@@ -1,68 +0,0 @@
-//go:build go1.18
-// +build go1.18
-
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-package pollers
-
-import (
-	"net/http"
-
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
-)
-
-// provisioningState returns the provisioning state from the response or the empty string.
-func provisioningState(jsonBody map[string]interface{}) string {
-	jsonProps, ok := jsonBody["properties"]
-	if !ok {
-		return ""
-	}
-	props, ok := jsonProps.(map[string]interface{})
-	if !ok {
-		return ""
-	}
-	rawPs, ok := props["provisioningState"]
-	if !ok {
-		return ""
-	}
-	ps, ok := rawPs.(string)
-	if !ok {
-		return ""
-	}
-	return ps
-}
-
-// status returns the status from the response or the empty string.
-func status(jsonBody map[string]interface{}) string {
-	rawStatus, ok := jsonBody["status"]
-	if !ok {
-		return ""
-	}
-	status, ok := rawStatus.(string)
-	if !ok {
-		return ""
-	}
-	return status
-}
-
-// GetStatus returns the LRO's status from the response body.
-// Typically used for Azure-AsyncOperation flows.
-// If there is no status in the response body the empty string is returned.
-func GetStatus(resp *http.Response) (string, error) {
-	jsonBody, err := pollers.GetJSON(resp)
-	if err != nil {
-		return "", err
-	}
-	return status(jsonBody), nil
-}
-
-// GetProvisioningState returns the LRO's state from the response body.
-// If there is no state in the response body the empty string is returned.
-func GetProvisioningState(resp *http.Response) (string, error) {
-	jsonBody, err := pollers.GetJSON(resp)
-	if err != nil {
-		return "", err
-	}
-	return provisioningState(jsonBody), nil
-}
diff --git a/sdk/azcore/arm/runtime/poller.go b/sdk/azcore/arm/runtime/poller.go
index 20cf91855d..e76c49625c 100644
--- a/sdk/azcore/arm/runtime/poller.go
+++ b/sdk/azcore/arm/runtime/poller.go
@@ -6,6 +6,8 @@
 
 package runtime
 
+// NOTE: this file will be deleted in a future release
+
 import (
 	"context"
 	"encoding/json"
@@ -14,10 +16,10 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/pollers/async"
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/pollers/body"
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/pollers/loc"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/armloc"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/async"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/body"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
 	"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
 )
@@ -50,6 +52,7 @@ type NewPollerOptions[T any] struct {
 }
 
 // NewPoller creates a Poller based on the provided initial response.
+// Deprecated: use azcore/runtime.NewPoller instead.
 func NewPoller[T any](resp *http.Response, pl runtime.Pipeline, options *NewPollerOptions[T]) (*Poller[T], error) {
 	if options == nil {
 		options = &NewPollerOptions[T]{}
@@ -68,8 +71,8 @@ func NewPoller[T any](resp *http.Response, pl runtime.Pipeline, options *NewPoll
 	var lro pollers.Operation
 	if async.Applicable(resp) {
 		lro, err = async.New(resp, options.FinalStateVia, tName)
-	} else if loc.Applicable(resp) {
-		lro, err = loc.New(resp, tName)
+	} else if armloc.Applicable(resp) {
+		lro, err = armloc.New(resp, tName)
 	} else if body.Applicable(resp) {
 		// must test body poller last as it's a subset of the other pollers.
 		// TODO: this is ambiguous for PATCH/PUT if it returns a 200 with no polling headers (sync completion)
@@ -98,6 +101,7 @@ type NewPollerFromResumeTokenOptions[T any] struct {
 }
 
 // NewPollerFromResumeToken creates a Poller from a resume token string.
+// Deprecated: use azcore/runtime.NewPollerFromResumeToken instead.
 func NewPollerFromResumeToken[T any](token string, pl runtime.Pipeline, options *NewPollerFromResumeTokenOptions[T]) (*Poller[T], error) {
 	if options == nil {
 		options = &NewPollerFromResumeTokenOptions[T]{}
@@ -116,9 +120,9 @@ func NewPollerFromResumeToken[T any](token string, pl runtime.Pipeline, options
 	case async.Kind:
 		log.Writef(log.EventLRO, "Resuming %s poller.", async.Kind)
 		lro = &async.Poller{}
-	case loc.Kind:
-		log.Writef(log.EventLRO, "Resuming %s poller.", loc.Kind)
-		lro = &loc.Poller{}
+	case armloc.Kind:
+		log.Writef(log.EventLRO, "Resuming %s poller.", armloc.Kind)
+		lro = &armloc.Poller{}
 	case body.Kind:
 		log.Writef(log.EventLRO, "Resuming %s poller.", body.Kind)
 		lro = &body.Poller{}
@@ -142,6 +146,7 @@ type PollUntilDoneOptions struct {
 }
 
 // Poller encapsulates a long-running operation, providing polling facilities until the operation reaches a terminal state.
+// Deprecated: use azcore/runtime.Poller[T] instead.
 type Poller[T any] struct {
 	pt *pollers.Poller
 	rt *T
diff --git a/sdk/azcore/arm/internal/pollers/loc/loc.go b/sdk/azcore/internal/pollers/armloc/loc.go
similarity index 91%
rename from sdk/azcore/arm/internal/pollers/loc/loc.go
rename to sdk/azcore/internal/pollers/armloc/loc.go
index ca5a953864..06fd2516dd 100644
--- a/sdk/azcore/arm/internal/pollers/loc/loc.go
+++ b/sdk/azcore/internal/pollers/armloc/loc.go
@@ -4,17 +4,16 @@
 // Copyright (c) Microsoft Corporation. All rights reserved.
 // Licensed under the MIT License.
 
-package loc
+package armloc
 
 import (
 	"errors"
 	"fmt"
 	"net/http"
 
-	armpollers "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/pollers"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
 	"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
 )
 
@@ -72,9 +71,9 @@ func (p *Poller) Update(resp *http.Response) error {
 	if h := resp.Header.Get(shared.HeaderLocation); h != "" {
 		p.PollURL = h
 	}
-	if runtime.HasStatusCode(resp, http.StatusOK, http.StatusCreated) {
+	if exported.HasStatusCode(resp, http.StatusOK, http.StatusCreated) {
 		// if a 200/201 returns a provisioning state, use that instead
-		state, err := armpollers.GetProvisioningState(resp)
+		state, err := pollers.GetProvisioningState(resp)
 		if err != nil && !errors.Is(err, pollers.ErrNoBody) {
 			return err
 		}
diff --git a/sdk/azcore/arm/internal/pollers/async/async.go b/sdk/azcore/internal/pollers/async/async.go
similarity index 95%
rename from sdk/azcore/arm/internal/pollers/async/async.go
rename to sdk/azcore/internal/pollers/async/async.go
index f6708c479a..053b54c4f6 100644
--- a/sdk/azcore/arm/internal/pollers/async/async.go
+++ b/sdk/azcore/internal/pollers/async/async.go
@@ -11,7 +11,6 @@ import (
 	"fmt"
 	"net/http"
 
-	armpollers "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/pollers"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
 	"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
@@ -68,7 +67,7 @@ func New(resp *http.Response, finalState pollers.FinalStateVia, pollerID string)
 		FinalState: finalState,
 	}
 	// check for provisioning state
-	state, err := armpollers.GetProvisioningState(resp)
+	state, err := pollers.GetProvisioningState(resp)
 	if errors.Is(err, pollers.ErrNoBody) || state == "" {
 		// NOTE: the ARM RPC spec explicitly states that for async PUT the initial response MUST
 		// contain a provisioning state.  to maintain compat with track 1 and other implementations
@@ -93,7 +92,7 @@ func (p *Poller) Done() bool {
 
 // Update updates the Poller from the polling response.
 func (p *Poller) Update(resp *http.Response) error {
-	state, err := armpollers.GetStatus(resp)
+	state, err := pollers.GetStatus(resp)
 	if err != nil {
 		return err
 	} else if state == "" {
diff --git a/sdk/azcore/arm/internal/pollers/async/async_test.go b/sdk/azcore/internal/pollers/async/async_test.go
similarity index 100%
rename from sdk/azcore/arm/internal/pollers/async/async_test.go
rename to sdk/azcore/internal/pollers/async/async_test.go
diff --git a/sdk/azcore/arm/internal/pollers/body/body.go b/sdk/azcore/internal/pollers/body/body.go
similarity index 94%
rename from sdk/azcore/arm/internal/pollers/body/body.go
rename to sdk/azcore/internal/pollers/body/body.go
index baf1fb90c3..eaa74de1c9 100644
--- a/sdk/azcore/arm/internal/pollers/body/body.go
+++ b/sdk/azcore/internal/pollers/body/body.go
@@ -10,7 +10,6 @@ import (
 	"errors"
 	"net/http"
 
-	armpollers "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/pollers"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
 	"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
 )
@@ -48,7 +47,7 @@ func New(resp *http.Response, pollerID string) (*Poller, error) {
 	// default initial state to InProgress.  depending on the HTTP
 	// status code and provisioning state, we might change the value.
 	curState := pollers.StatusInProgress
-	provState, err := armpollers.GetProvisioningState(resp)
+	provState, err := pollers.GetProvisioningState(resp)
 	if err != nil && !errors.Is(err, pollers.ErrNoBody) {
 		return nil, err
 	}
@@ -85,7 +84,7 @@ func (p *Poller) Update(resp *http.Response) error {
 		p.CurState = pollers.StatusSucceeded
 		return nil
 	}
-	state, err := armpollers.GetProvisioningState(resp)
+	state, err := pollers.GetProvisioningState(resp)
 	if errors.Is(err, pollers.ErrNoBody) {
 		// a missing response body in non-204 case is an error
 		return err
diff --git a/sdk/azcore/arm/internal/pollers/body/body_test.go b/sdk/azcore/internal/pollers/body/body_test.go
similarity index 100%
rename from sdk/azcore/arm/internal/pollers/body/body_test.go
rename to sdk/azcore/internal/pollers/body/body_test.go
diff --git a/sdk/azcore/internal/pollers/util.go b/sdk/azcore/internal/pollers/util.go
index e7012cc904..4dd046597b 100644
--- a/sdk/azcore/internal/pollers/util.go
+++ b/sdk/azcore/internal/pollers/util.go
@@ -116,6 +116,61 @@ func GetJSON(resp *http.Response) (map[string]interface{}, error) {
 	return jsonBody, nil
 }
 
+// provisioningState returns the provisioning state from the response or the empty string.
+func provisioningState(jsonBody map[string]interface{}) string {
+	jsonProps, ok := jsonBody["properties"]
+	if !ok {
+		return ""
+	}
+	props, ok := jsonProps.(map[string]interface{})
+	if !ok {
+		return ""
+	}
+	rawPs, ok := props["provisioningState"]
+	if !ok {
+		return ""
+	}
+	ps, ok := rawPs.(string)
+	if !ok {
+		return ""
+	}
+	return ps
+}
+
+// status returns the status from the response or the empty string.
+func status(jsonBody map[string]interface{}) string {
+	rawStatus, ok := jsonBody["status"]
+	if !ok {
+		return ""
+	}
+	status, ok := rawStatus.(string)
+	if !ok {
+		return ""
+	}
+	return status
+}
+
+// GetStatus returns the LRO's status from the response body.
+// Typically used for Azure-AsyncOperation flows.
+// If there is no status in the response body the empty string is returned.
+func GetStatus(resp *http.Response) (string, error) {
+	jsonBody, err := GetJSON(resp)
+	if err != nil {
+		return "", err
+	}
+	return status(jsonBody), nil
+}
+
+// GetProvisioningState returns the LRO's state from the response body.
+// If there is no state in the response body the empty string is returned.
+func GetProvisioningState(resp *http.Response) (string, error) {
+	jsonBody, err := GetJSON(resp)
+	if err != nil {
+		return "", err
+	}
+	return provisioningState(jsonBody), nil
+}
+
 // used if the operation synchronously completed
 type NopPoller struct{}
 
diff --git a/sdk/azcore/runtime/poller.go b/sdk/azcore/runtime/poller.go
index 75869d8e1f..c8fd5f448e 100644
--- a/sdk/azcore/runtime/poller.go
+++ b/sdk/azcore/runtime/poller.go
@@ -16,6 +16,9 @@ import (
 
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/armloc"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/async"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/body"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/loc"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/op"
 	"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
@@ -65,14 +68,28 @@ func NewPoller[T any](resp *http.Response, pl exported.Pipeline, options *NewPol
 	}
 	// determine the polling method
 	var lro pollers.Operation
-	// op poller must be checked first as it can also have a location header
-	if op.Applicable(resp) {
+	if async.Applicable(resp) {
+		// async poller must be checked first as it can also have a location header
+		lro, err = async.New(resp, options.FinalStateVia, tName)
+	} else if op.Applicable(resp) {
+		// op poller must be checked before loc as it can also have a location header
 		lro, err = op.New(resp, options.FinalStateVia, tName)
+	} else if armloc.Applicable(resp) {
+		lro, err = armloc.New(resp, tName)
 	} else if loc.Applicable(resp) {
 		lro, err = loc.New(resp, options.FinalStateVia, tName)
+	} else if body.Applicable(resp) {
+		// must test body poller last as it's a subset of the other pollers.
+		// TODO: this is ambiguous for PATCH/PUT if it returns a 200 with no polling headers (sync completion)
+		lro, err = body.New(resp, tName)
+	} else if m := resp.Request.Method; resp.StatusCode == http.StatusAccepted && (m == http.MethodDelete || m == http.MethodPost) {
+		// if we get here it means we have a 202 with no polling headers.
+		// for DELETE and POST this is a hard error per ARM RPC spec.
+		return nil, errors.New("response is missing polling URL")
 	} else {
 		lro = &pollers.NopPoller{}
 	}
+
 	if err != nil {
 		return nil, err
 	}
@@ -105,6 +122,15 @@ func NewPollerFromResumeToken[T any](token string, pl exported.Pipeline, options
 	// now rehydrate the poller based on the encoded poller type
 	var lro pollers.Operation
 	switch kind {
+	case async.Kind:
+		log.Writef(log.EventLRO, "Resuming %s poller.", async.Kind)
+		lro = &async.Poller{}
+	case armloc.Kind:
+		log.Writef(log.EventLRO, "Resuming %s poller.", armloc.Kind)
+		lro = &armloc.Poller{}
+	case body.Kind:
+		log.Writef(log.EventLRO, "Resuming %s poller.", body.Kind)
+		lro = &body.Poller{}
 	case loc.Kind:
 		log.Writef(log.EventLRO, "Resuming %s poller.", loc.Kind)
 		lro = &loc.Poller{}


Test patch:
diff --git a/sdk/azcore/arm/internal/pollers/pollers_test.go b/sdk/azcore/arm/internal/pollers/pollers_test.go
deleted file mode 100644
index 307ce15a46..0000000000
--- a/sdk/azcore/arm/internal/pollers/pollers_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-//go:build go1.18
-// +build go1.18
-
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-
-package pollers
-
-import (
-	"errors"
-	"io/ioutil"
-	"net/http"
-	"strings"
-	"testing"
-
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
-)
-
-func TestGetStatusSuccess(t *testing.T) {
-	const jsonBody = `{ "status": "InProgress" }`
-	resp := &http.Response{
-		Body: ioutil.NopCloser(strings.NewReader(jsonBody)),
-	}
-	status, err := GetStatus(resp)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if status != "InProgress" {
-		t.Fatalf("unexpected status %s", status)
-	}
-}
-
-func TestGetNoBody(t *testing.T) {
-	resp := &http.Response{
-		Body: http.NoBody,
-	}
-	status, err := GetStatus(resp)
-	if !errors.Is(err, pollers.ErrNoBody) {
-		t.Fatalf("unexpected error %T", err)
-	}
-	if status != "" {
-		t.Fatal("expected empty status")
-	}
-	status, err = GetProvisioningState(resp)
-	if !errors.Is(err, pollers.ErrNoBody) {
-		t.Fatalf("unexpected error %T", err)
-	}
-	if status != "" {
-		t.Fatal("expected empty status")
-	}
-}
-
-func TestGetStatusError(t *testing.T) {
-	resp := &http.Response{
-		Body: ioutil.NopCloser(strings.NewReader("{}")),
-	}
-	status, err := GetStatus(resp)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if status != "" {
-		t.Fatalf("expected empty status, got %s", status)
-	}
-}
-
-func TestGetProvisioningState(t *testing.T) {
-	const jsonBody = `{ "properties": { "provisioningState": "Canceled" } }`
-	resp := &http.Response{
-		Body: ioutil.NopCloser(strings.NewReader(jsonBody)),
-	}
-	state, err := GetProvisioningState(resp)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if state != "Canceled" {
-		t.Fatalf("unexpected status %s", state)
-	}
-}
-
-func TestGetProvisioningStateError(t *testing.T) {
-	resp := &http.Response{
-		Body: ioutil.NopCloser(strings.NewReader("{}")),
-	}
-	state, err := GetProvisioningState(resp)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if state != "" {
-		t.Fatalf("expected empty provisioning state, got %s", state)
-	}
-}
diff --git a/sdk/azcore/arm/runtime/poller_test.go b/sdk/azcore/arm/runtime/poller_test.go
index 7fd30e68d3..d531312621 100644
--- a/sdk/azcore/arm/runtime/poller_test.go
+++ b/sdk/azcore/arm/runtime/poller_test.go
@@ -15,11 +15,11 @@ import (
 	"testing"
 	"time"
 
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/pollers/async"
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/pollers/body"
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/pollers/loc"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/armloc"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/async"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/body"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
@@ -153,7 +153,7 @@ func TestNewPollerLoc(t *testing.T) {
 	if !closed() {
 		t.Fatal("initial response body wasn't closed")
 	}
-	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&loc.Poller{}) {
+	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&armloc.Poller{}) {
 		t.Fatalf("unexpected poller type %s", pt.String())
 	}
 	tk, err := poller.ResumeToken()
diff --git a/sdk/azcore/arm/internal/pollers/loc/loc_test.go b/sdk/azcore/internal/pollers/armloc/loc_test.go
similarity index 99%
rename from sdk/azcore/arm/internal/pollers/loc/loc_test.go
rename to sdk/azcore/internal/pollers/armloc/loc_test.go
index 260140aef6..b0fc584e83 100644
--- a/sdk/azcore/arm/internal/pollers/loc/loc_test.go
+++ b/sdk/azcore/internal/pollers/armloc/loc_test.go
@@ -4,7 +4,7 @@
 // Copyright (c) Microsoft Corporation. All rights reserved.
 // Licensed under the MIT License.
 
-package loc
+package armloc
 
 import (
 	"io"
diff --git a/sdk/azcore/internal/pollers/util_test.go b/sdk/azcore/internal/pollers/util_test.go
index 5255b425c6..fcce329868 100644
--- a/sdk/azcore/internal/pollers/util_test.go
+++ b/sdk/azcore/internal/pollers/util_test.go
@@ -170,6 +170,80 @@ func TestGetJSON(t *testing.T) {
 	}
 }
 
+func TestGetStatusSuccess(t *testing.T) {
+	const jsonBody = `{ "status": "InProgress" }`
+	resp := &http.Response{
+		Body: ioutil.NopCloser(strings.NewReader(jsonBody)),
+	}
+	status, err := GetStatus(resp)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if status != "InProgress" {
+		t.Fatalf("unexpected status %s", status)
+	}
+}
+
+func TestGetNoBody(t *testing.T) {
+	resp := &http.Response{
+		Body: http.NoBody,
+	}
+	status, err := GetStatus(resp)
+	if !errors.Is(err, ErrNoBody) {
+		t.Fatalf("unexpected error %T", err)
+	}
+	if status != "" {
+		t.Fatal("expected empty status")
+	}
+	status, err = GetProvisioningState(resp)
+	if !errors.Is(err, ErrNoBody) {
+		t.Fatalf("unexpected error %T", err)
+	}
+	if status != "" {
+		t.Fatal("expected empty status")
+	}
+}
+
+func TestGetStatusError(t *testing.T) {
+	resp := &http.Response{
+		Body: ioutil.NopCloser(strings.NewReader("{}")),
+	}
+	status, err := GetStatus(resp)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if status != "" {
+		t.Fatalf("expected empty status, got %s", status)
+	}
+}
+
+func TestGetProvisioningState(t *testing.T) {
+	const jsonBody = `{ "properties": { "provisioningState": "Canceled" } }`
+	resp := &http.Response{
+		Body: ioutil.NopCloser(strings.NewReader(jsonBody)),
+	}
+	state, err := GetProvisioningState(resp)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if state != "Canceled" {
+		t.Fatalf("unexpected status %s", state)
+	}
+}
+
+func TestGetProvisioningStateError(t *testing.T) {
+	resp := &http.Response{
+		Body: ioutil.NopCloser(strings.NewReader("{}")),
+	}
+	state, err := GetProvisioningState(resp)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if state != "" {
+		t.Fatalf("expected empty provisioning state, got %s", state)
+	}
+}
+
 func TestNopPoller(t *testing.T) {
 	np := NopPoller{}
 	if !np.Done() {
diff --git a/sdk/azcore/runtime/poller_test.go b/sdk/azcore/runtime/poller_test.go
index e15b2cca21..ab092597fc 100644
--- a/sdk/azcore/runtime/poller_test.go
+++ b/sdk/azcore/runtime/poller_test.go
@@ -10,14 +10,20 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io"
 	"net/http"
 	"net/url"
 	"reflect"
+	"strings"
 	"testing"
 	"time"
 
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/armloc"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/async"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/pollers/body"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
 	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
 	"github.com/Azure/azure-sdk-for-go/sdk/internal/mock"
 )
@@ -566,8 +572,16 @@ func TestOpPollerWithResumeToken(t *testing.T) {
 }
 
 func TestNopPoller(t *testing.T) {
+	reqURL, err := url.Parse("https://fake.endpoint/for/testing")
+	if err != nil {
+		t.Fatal(err)
+	}
 	firstResp := &http.Response{
 		StatusCode: http.StatusOK,
+		Request: &http.Request{
+			Method: http.MethodDelete,
+			URL:    reqURL,
+		},
 	}
 	body, closed := mock.NewTrackedCloser(http.NoBody)
 	firstResp.Body = body
@@ -659,3 +673,352 @@ func TestOpPollerWithResponseType(t *testing.T) {
 		t.Fatalf("unexpected widget size %d", w.Preconstructed)
 	}
 }
+
+const (
+	provStateStarted   = `{ "properties": { "provisioningState": "Started" } }`
+	provStateUpdating  = `{ "properties": { "provisioningState": "Updating" } }`
+	provStateSucceeded = `{ "properties": { "provisioningState": "Succeeded" }, "field": "value" }`
+	provStateFailed    = `{ "properties": { "provisioningState": "Failed" } }` //nolint
+	statusInProgress   = `{ "status": "InProgress" }`
+	statusSucceeded    = `{ "status": "Succeeded" }`
+	statusCanceled     = `{ "status": "Canceled" }`
+	successResp        = `{ "field": "value" }`
+	errorResp          = `{ "error": "the operation failed" }`
+)
+
+type mockType struct {
+	Field *string `json:"field,omitempty"`
+}
+
+func getPipeline(srv *mock.Server) Pipeline {
+	return NewPipeline(
+		"test",
+		"v0.1.0",
+		PipelineOptions{PerRetry: []policy.Policy{NewLogPolicy(nil)}},
+		&policy.ClientOptions{Transport: srv},
+	)
+}
+
+func initialResponse(method, u string, resp io.Reader) (*http.Response, mock.TrackedClose) {
+	req, err := http.NewRequest(method, u, nil)
+	if err != nil {
+		panic(err)
+	}
+	body, closed := mock.NewTrackedCloser(resp)
+	return &http.Response{
+		Body:          body,
+		ContentLength: -1,
+		Header:        http.Header{},
+		Request:       req,
+	}, closed
+}
+
+func TestNewPollerAsync(t *testing.T) {
+	srv, close := mock.NewServer()
+	defer close()
+	srv.AppendResponse(mock.WithBody([]byte(statusInProgress)))
+	srv.AppendResponse(mock.WithBody([]byte(statusSucceeded)))
+	srv.AppendResponse(mock.WithBody([]byte(successResp)))
+	resp, closed := initialResponse(http.MethodPut, srv.URL(), strings.NewReader(provStateStarted))
+	resp.Header.Set(shared.HeaderAzureAsync, srv.URL())
+	resp.StatusCode = http.StatusCreated
+	pl := getPipeline(srv)
+	poller, err := NewPoller[mockType](resp, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !closed() {
+		t.Fatal("initial response body wasn't closed")
+	}
+	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&async.Poller{}) {
+		t.Fatalf("unexpected poller type %s", pt.String())
+	}
+	tk, err := poller.ResumeToken()
+	if err != nil {
+		t.Fatal(err)
+	}
+	poller, err = NewPollerFromResumeToken[mockType](tk, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	result, err := poller.PollUntilDone(context.Background(), &PollUntilDoneOptions{Frequency: time.Second})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if v := *result.Field; v != "value" {
+		t.Fatalf("unexpected value %s", v)
+	}
+}
+
+func TestNewPollerBody(t *testing.T) {
+	srv, close := mock.NewServer()
+	defer close()
+	srv.AppendResponse(mock.WithBody([]byte(provStateUpdating)), mock.WithHeader("Retry-After", "1"))
+	srv.AppendResponse(mock.WithBody([]byte(provStateSucceeded)))
+	resp, closed := initialResponse(http.MethodPatch, srv.URL(), strings.NewReader(provStateStarted))
+	resp.StatusCode = http.StatusCreated
+	pl := getPipeline(srv)
+	poller, err := NewPoller[mockType](resp, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !closed() {
+		t.Fatal("initial response body wasn't closed")
+	}
+	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&body.Poller{}) {
+		t.Fatalf("unexpected poller type %s", pt.String())
+	}
+	tk, err := poller.ResumeToken()
+	if err != nil {
+		t.Fatal(err)
+	}
+	poller, err = NewPollerFromResumeToken[mockType](tk, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	result, err := poller.PollUntilDone(context.Background(), &PollUntilDoneOptions{Frequency: time.Second})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if v := *result.Field; v != "value" {
+		t.Fatalf("unexpected value %s", v)
+	}
+}
+
+func TestNewPollerLoc(t *testing.T) {
+	srv, close := mock.NewServer()
+	defer close()
+	srv.AppendResponse(mock.WithStatusCode(http.StatusAccepted))
+	srv.AppendResponse(mock.WithBody([]byte(successResp)))
+	resp, closed := initialResponse(http.MethodPatch, srv.URL(), strings.NewReader(provStateStarted))
+	resp.Header.Set(shared.HeaderLocation, srv.URL())
+	resp.StatusCode = http.StatusAccepted
+	pl := getPipeline(srv)
+	poller, err := NewPoller[mockType](resp, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !closed() {
+		t.Fatal("initial response body wasn't closed")
+	}
+	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&armloc.Poller{}) {
+		t.Fatalf("unexpected poller type %s", pt.String())
+	}
+	tk, err := poller.ResumeToken()
+	if err != nil {
+		t.Fatal(err)
+	}
+	poller, err = NewPollerFromResumeToken[mockType](tk, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	for {
+		if poller.Done() {
+			break
+		}
+		_, err = poller.Poll(context.Background())
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	result, err := poller.Result(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if v := *result.Field; v != "value" {
+		t.Fatalf("unexpected value %s", v)
+	}
+}
+
+func TestNewPollerInitialRetryAfter(t *testing.T) {
+	srv, close := mock.NewServer()
+	defer close()
+	srv.AppendResponse(mock.WithBody([]byte(statusInProgress)))
+	srv.AppendResponse(mock.WithBody([]byte(statusSucceeded)))
+	srv.AppendResponse(mock.WithBody([]byte(successResp)))
+	resp, closed := initialResponse(http.MethodPut, srv.URL(), strings.NewReader(provStateStarted))
+	resp.Header.Set(shared.HeaderAzureAsync, srv.URL())
+	resp.Header.Set("Retry-After", "1")
+	resp.StatusCode = http.StatusCreated
+	pl := getPipeline(srv)
+	poller, err := NewPoller[mockType](resp, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !closed() {
+		t.Fatal("initial response body wasn't closed")
+	}
+	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&async.Poller{}) {
+		t.Fatalf("unexpected poller type %s", pt.String())
+	}
+	result, err := poller.PollUntilDone(context.Background(), &PollUntilDoneOptions{Frequency: time.Second})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if v := *result.Field; v != "value" {
+		t.Fatalf("unexpected value %s", v)
+	}
+}
+
+func TestNewPollerCanceled(t *testing.T) {
+	srv, close := mock.NewServer()
+	defer close()
+	srv.AppendResponse(mock.WithBody([]byte(statusInProgress)))
+	srv.AppendResponse(mock.WithBody([]byte(statusCanceled)), mock.WithStatusCode(http.StatusOK))
+	resp, closed := initialResponse(http.MethodPut, srv.URL(), strings.NewReader(provStateStarted))
+	resp.Header.Set(shared.HeaderAzureAsync, srv.URL())
+	resp.StatusCode = http.StatusCreated
+	pl := getPipeline(srv)
+	poller, err := NewPoller[mockType](resp, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !closed() {
+		t.Fatal("initial response body wasn't closed")
+	}
+	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&async.Poller{}) {
+		t.Fatalf("unexpected poller type %s", pt.String())
+	}
+	_, err = poller.Poll(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
+	if poller.Done() {
+		t.Fatal("poller shouldn't be done yet")
+	}
+	_, err = poller.Poll(context.Background())
+	if err == nil {
+		t.Fatal("unexpected nil error")
+	}
+	if !poller.Done() {
+		t.Fatal("poller should be done")
+	}
+}
+
+func TestNewPollerFailedWithError(t *testing.T) {
+	srv, close := mock.NewServer()
+	defer close()
+	srv.AppendResponse(mock.WithBody([]byte(statusInProgress)))
+	srv.AppendResponse(mock.WithBody([]byte(errorResp)), mock.WithStatusCode(http.StatusBadRequest))
+	resp, closed := initialResponse(http.MethodPut, srv.URL(), strings.NewReader(provStateStarted))
+	resp.Header.Set(shared.HeaderAzureAsync, srv.URL())
+	resp.StatusCode = http.StatusCreated
+	pl := getPipeline(srv)
+	poller, err := NewPoller[mockType](resp, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !closed() {
+		t.Fatal("initial response body wasn't closed")
+	}
+	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&async.Poller{}) {
+		t.Fatalf("unexpected poller type %s", pt.String())
+	}
+	_, err = poller.PollUntilDone(context.Background(), &PollUntilDoneOptions{Frequency: time.Second})
+	if err == nil {
+		t.Fatal(err)
+	}
+	if _, ok := err.(*exported.ResponseError); !ok {
+		t.Fatalf("unexpected error type %T", err)
+	}
+}
+
+func TestNewPollerSuccessNoContent(t *testing.T) {
+	srv, close := mock.NewServer()
+	defer close()
+	srv.AppendResponse(mock.WithBody([]byte(provStateUpdating)))
+	srv.AppendResponse(mock.WithStatusCode(http.StatusNoContent))
+	resp, closed := initialResponse(http.MethodPatch, srv.URL(), strings.NewReader(provStateStarted))
+	resp.StatusCode = http.StatusCreated
+	pl := getPipeline(srv)
+	poller, err := NewPoller[mockType](resp, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !closed() {
+		t.Fatal("initial response body wasn't closed")
+	}
+	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&body.Poller{}) {
+		t.Fatalf("unexpected poller type %s", pt.String())
+	}
+	tk, err := poller.ResumeToken()
+	if err != nil {
+		t.Fatal(err)
+	}
+	poller, err = NewPollerFromResumeToken[mockType](tk, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	result, err := poller.PollUntilDone(context.Background(), &PollUntilDoneOptions{Frequency: time.Second})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if result.Field != nil {
+		t.Fatal("expected nil result")
+	}
+}
+
+func TestNewPollerFail202NoHeaders(t *testing.T) {
+	srv, close := mock.NewServer()
+	defer close()
+	resp, closed := initialResponse(http.MethodDelete, srv.URL(), http.NoBody)
+	resp.StatusCode = http.StatusAccepted
+	pl := getPipeline(srv)
+	poller, err := NewPoller[mockType](resp, pl, nil)
+	if err == nil {
+		t.Fatal("unexpected nil error")
+	}
+	if !closed() {
+		t.Fatal("initial response body wasn't closed")
+	}
+	if poller != nil {
+		t.Fatal("expected nil poller")
+	}
+}
+
+type preconstructedMockType struct {
+	Field          *string `json:"field,omitempty"`
+	Preconstructed int
+}
+
+func TestNewPollerWithResponseType(t *testing.T) {
+	srv, close := mock.NewServer()
+	defer close()
+	srv.AppendResponse(mock.WithBody([]byte(provStateUpdating)), mock.WithHeader("Retry-After", "1"))
+	srv.AppendResponse(mock.WithBody([]byte(provStateSucceeded)))
+	resp, closed := initialResponse(http.MethodPatch, srv.URL(), strings.NewReader(provStateStarted))
+	resp.StatusCode = http.StatusCreated
+	pl := getPipeline(srv)
+	poller, err := NewPoller[preconstructedMockType](resp, pl, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !closed() {
+		t.Fatal("initial response body wasn't closed")
+	}
+	if pt := pollers.PollerType(poller.pt); pt != reflect.TypeOf(&body.Poller{}) {
+		t.Fatalf("unexpected poller type %s", pt.String())
+	}
+	tk, err := poller.ResumeToken()
+	if err != nil {
+		t.Fatal(err)
+	}
+	poller, err = NewPollerFromResumeToken(tk, pl, &NewPollerFromResumeTokenOptions[preconstructedMockType]{
+		Response: &preconstructedMockType{
+			Preconstructed: 12345,
+		},
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	result, err := poller.PollUntilDone(context.Background(), &PollUntilDoneOptions{Frequency: time.Second})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if v := *result.Field; v != "value" {
+		t.Fatalf("unexpected value %s", v)
+	}
+	if result.Preconstructed != 12345 {
+		t.Fatalf("unexpected value %d", result.Preconstructed)
+	}
+}