@@ -15,15 +15,22 @@ import (
1515 "testing"
1616
1717 "github.com/azure/azure-dev/cli/azd/internal"
18+ "github.com/azure/azure-dev/cli/azd/internal/tracing/events"
19+ "github.com/azure/azure-dev/cli/azd/internal/tracing/fields"
1820 "github.com/azure/azure-dev/cli/azd/pkg/config"
1921 "github.com/azure/azure-dev/cli/azd/pkg/exec"
2022 "github.com/azure/azure-dev/cli/azd/pkg/extensions"
23+ "github.com/azure/azure-dev/cli/azd/pkg/input"
2124 "github.com/azure/azure-dev/cli/azd/pkg/lazy"
2225 "github.com/azure/azure-dev/cli/azd/pkg/output"
2326 "github.com/azure/azure-dev/cli/azd/test/mocks"
2427 "github.com/azure/azure-dev/cli/azd/test/mocks/mockinput"
2528 "github.com/stretchr/testify/assert"
2629 "github.com/stretchr/testify/require"
30+ "go.opentelemetry.io/otel"
31+ "go.opentelemetry.io/otel/attribute"
32+ tracesdk "go.opentelemetry.io/otel/sdk/trace"
33+ "go.opentelemetry.io/otel/sdk/trace/tracetest"
2734)
2835
2936// ---------------------------------------------------------------------------
@@ -439,30 +446,39 @@ func TestUpgradeOneExtension(t *testing.T) {
439446 const registryURL = "https://test.example.com/registry.json"
440447
441448 tests := []struct {
442- name string
443- extensionId string
444- installed map [string ]* extensions.Extension
445- registry extensions.Registry
446- flags extensionUpgradeFlags
447- wantStatus extensions.UpgradeStatus
448- wantErr string
449- wantErrSubstr string
450- wantSkipReason string
449+ name string
450+ extensionId string
451+ installed map [string ]* extensions.Extension
452+ registry extensions.Registry
453+ flags extensionUpgradeFlags
454+ wantStatus extensions.UpgradeStatus
455+ wantErr string
456+ wantErrSubstr string
457+ wantSkipReason string
458+ wantFromSourceCategory extensions.SourceCategory
459+ wantToSourceCategory extensions.SourceCategory
451460 }{
452461 {
453462 name : "skip_already_up_to_date" ,
454463 extensionId : "ext-a" ,
455464 installed : map [string ]* extensions.Extension {
456- "ext-a" : {Id : "ext-a" , Version : "1.0.0" , Source : "test" },
465+ "ext-a" : {
466+ Id : "ext-a" ,
467+ Version : "1.0.0" ,
468+ Source : "test" ,
469+ SourceCategory : extensions .SourceCategoryLocal ,
470+ },
457471 },
458472 registry : testRegistry (
459473 testExtMeta ("ext-a" , "1.0.0" , "test" ),
460474 ),
461475 flags : extensionUpgradeFlags {
462476 global : & internal.GlobalCommandOptions {NoPrompt : true },
463477 },
464- wantStatus : extensions .UpgradeStatusSkipped ,
465- wantSkipReason : "already up to date" ,
478+ wantStatus : extensions .UpgradeStatusSkipped ,
479+ wantSkipReason : "already up to date" ,
480+ wantFromSourceCategory : extensions .SourceCategoryLocal ,
481+ wantToSourceCategory : extensions .SourceCategoryOther ,
466482 },
467483 {
468484 name : "skip_installed_is_newer" ,
@@ -483,14 +499,21 @@ func TestUpgradeOneExtension(t *testing.T) {
483499 name : "skipped_delisted_extension" ,
484500 extensionId : "missing-ext" ,
485501 installed : map [string ]* extensions.Extension {
486- "missing-ext" : {Id : "missing-ext" , Version : "1.0.0" , Source : "test" },
502+ "missing-ext" : {
503+ Id : "missing-ext" ,
504+ Version : "1.0.0" ,
505+ Source : "test" ,
506+ SourceCategory : extensions .SourceCategoryDev ,
507+ },
487508 },
488509 registry : testRegistry (), // empty registry
489510 flags : extensionUpgradeFlags {
490511 global : & internal.GlobalCommandOptions {NoPrompt : true },
491512 },
492- wantStatus : extensions .UpgradeStatusSkipped ,
493- wantSkipReason : "extension no longer available in any configured registry" ,
513+ wantStatus : extensions .UpgradeStatusSkipped ,
514+ wantSkipReason : "extension no longer available in any configured registry" ,
515+ wantFromSourceCategory : extensions .SourceCategoryDev ,
516+ wantToSourceCategory : extensions .SourceCategoryDev ,
494517 },
495518 {
496519 name : "failed_no_stored_or_main_source_match" ,
@@ -662,10 +685,149 @@ func TestUpgradeOneExtension(t *testing.T) {
662685 if tt .wantSkipReason != "" {
663686 assert .Equal (t , tt .wantSkipReason , result .SkipReason )
664687 }
688+ if tt .wantFromSourceCategory != "" {
689+ assert .Equal (t , tt .wantFromSourceCategory , result .FromSourceCategory )
690+ }
691+ if tt .wantToSourceCategory != "" {
692+ assert .Equal (t , tt .wantToSourceCategory , result .ToSourceCategory )
693+ }
665694 })
666695 }
667696}
668697
698+ func TestExtensionLifecycleTelemetrySpans (t * testing.T ) {
699+ recorder := tracetest .NewSpanRecorder ()
700+ provider := tracesdk .NewTracerProvider (tracesdk .WithSpanProcessor (recorder ))
701+ previousProvider := otel .GetTracerProvider ()
702+ otel .SetTracerProvider (provider )
703+ t .Cleanup (func () { otel .SetTracerProvider (previousProvider ) })
704+
705+ t .Run ("UnresolvedUpgradeUsesPersistedCategory" , func (t * testing.T ) {
706+ const registryURL = "https://private.example/registry.json"
707+ const sourceName = "private-source"
708+
709+ mockContext := mocks .NewMockContext (t .Context ())
710+ manager , sourceManager := createUpgradeTestManager (
711+ t ,
712+ mockContext ,
713+ map [string ]* extensions.Extension {
714+ "missing-ext" : {
715+ Id : "missing-ext" ,
716+ Version : "1.0.0" ,
717+ Source : sourceName ,
718+ SourceCategory : extensions .SourceCategoryDev ,
719+ },
720+ },
721+ registryURL ,
722+ testRegistry (),
723+ )
724+ action := & extensionUpgradeAction {
725+ args : []string {"missing-ext" },
726+ flags : & extensionUpgradeFlags {
727+ global : & internal.GlobalCommandOptions {NoPrompt : true },
728+ },
729+ formatter : & output.JsonFormatter {},
730+ writer : & bytes.Buffer {},
731+ console : mockinput .NewMockConsole (),
732+ sourceManager : sourceManager ,
733+ extensionManager : manager ,
734+ }
735+
736+ result := action .upgradeOneExtension (t .Context (), "missing-ext" , 0 , nil , true )
737+ require .Equal (t , extensions .UpgradeStatusSkipped , result .Status )
738+
739+ span := extensionEndedSpan (t , recorder , events .ExtensionUpdateEvent )
740+ require .Equal (
741+ t ,
742+ string (extensions .SourceCategoryDev ),
743+ extensionSpanAttribute (t , span .Attributes (), fields .ExtensionSourceCategory .Key ).Value .AsString (),
744+ )
745+ for _ , attr := range span .Attributes () {
746+ require .NotContains (t , attr .Value .Emit (), sourceName )
747+ require .NotContains (t , attr .Value .Emit (), registryURL )
748+ }
749+ })
750+
751+ t .Run ("PromotionUsesFixedCategories" , func (t * testing.T ) {
752+ emitPromotionEvent (
753+ t .Context (),
754+ "test.extension" ,
755+ "1.0.0" ,
756+ "1.1.0" ,
757+ extensions .SourceCategoryDev ,
758+ extensions .SourceCategoryAzd ,
759+ )
760+
761+ span := extensionEndedSpan (t , recorder , events .ExtensionPromoteEvent )
762+ require .Equal (
763+ t ,
764+ string (extensions .SourceCategoryDev ),
765+ extensionSpanAttribute (t , span .Attributes (), fields .ExtensionSourceCategoryFrom .Key ).Value .AsString (),
766+ )
767+ require .Equal (
768+ t ,
769+ string (extensions .SourceCategoryAzd ),
770+ extensionSpanAttribute (t , span .Attributes (), fields .ExtensionSourceCategoryTo .Key ).Value .AsString (),
771+ )
772+ })
773+ }
774+
775+ func TestDisplayPromotionWarning (t * testing.T ) {
776+ t .Parallel ()
777+
778+ console := mockinput .NewMockConsole ()
779+ action := & extensionUpgradeAction {console : console }
780+ action .displayPromotionWarning (
781+ t .Context (),
782+ "Updating test.extension extension" ,
783+ "test.extension" ,
784+ "1.0.0" ,
785+ "1.1.0" ,
786+ "dev" ,
787+ "azd" ,
788+ )
789+
790+ require .Len (t , console .SpinnerOps (), 1 )
791+ require .Equal (t , input .StepWarning , console .SpinnerOps ()[0 ].Format )
792+ rendered := strings .Join (console .Output (), "\n " )
793+ require .Contains (t , rendered , "Updated test.extension extension" )
794+ require .Contains (t , rendered , "1.0.0" )
795+ require .Contains (t , rendered , "1.1.0" )
796+ require .Contains (t , rendered , "promoted from the dev registry" )
797+ require .Contains (t , rendered , "official azd registry" )
798+ require .Contains (t , rendered , "azd extension install test.extension --source dev" )
799+ }
800+
801+ func extensionEndedSpan (
802+ t * testing.T ,
803+ recorder * tracetest.SpanRecorder ,
804+ name string ,
805+ ) tracesdk.ReadOnlySpan {
806+ t .Helper ()
807+ for _ , span := range recorder .Ended () {
808+ if span .Name () == name {
809+ return span
810+ }
811+ }
812+ require .FailNow (t , "telemetry span not found" , "name: %s" , name )
813+ return nil
814+ }
815+
816+ func extensionSpanAttribute (
817+ t * testing.T ,
818+ attributes []attribute.KeyValue ,
819+ key attribute.Key ,
820+ ) attribute.KeyValue {
821+ t .Helper ()
822+ for _ , attr := range attributes {
823+ if attr .Key == key {
824+ return attr
825+ }
826+ }
827+ require .FailNow (t , "telemetry attribute not found" , "key: %s" , key )
828+ return attribute.KeyValue {}
829+ }
830+
669831// TestUpgradeAction_MixedBatch tests a batch with some skip, some fail.
670832func TestUpgradeAction_MixedBatch (t * testing.T ) {
671833 t .Parallel ()
0 commit comments