diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index c2d0f41..36e6e0f 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -44,10 +44,6 @@ jobs: - name: Test with pytest run: | python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:testcov - python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:pytest tests/test_migrations_engine_hooks_lifetime.py - python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:pytest tests/test_migrations_engine_hooks_filters_lifetime.py - python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:pytest tests/test_migrations_engine_hooks_mappings_lifetime.py - python -m hatch --data-dir=.hatch --cache-dir=.hatch_cache run test:pytest tests/test_migrations_engine_hooks_transformers_lifetime.py - name: Test TestApplication with pytest run: | diff --git a/.gitignore b/.gitignore index 3e6a492..c0e14ea 100644 --- a/.gitignore +++ b/.gitignore @@ -176,8 +176,8 @@ appsettings.Development.json clean-server-settings.dev.json launchSettings.json UpgradeLog.htm -*.DEV.ini -*.DEV.json +*.*.ini +*.*.json /src/Python/Documentation/_build /src/Python/Documentation/_static /src/Python/Python.pyproj.user diff --git a/Directory.Build.props b/Directory.Build.props index 1c24409..d009a18 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,9 +4,10 @@ enable true true - 3.0.1 + 4.0.0 Salesforce, Inc. Salesforce, Inc. Copyright (c) 2024, Salesforce, Inc. and its licensors + Apache-2.0 \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index b2949c4..ae7332a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ Apache License Version 2.0 -Copyright (c) 2023 Salesforce, Inc. +Copyright (c) 2024 Salesforce, Inc. All rights reserved. Apache License diff --git a/Migration SDK.sln b/Migration SDK.sln index 19e5b58..6ad00fc 100644 --- a/Migration SDK.sln +++ b/Migration SDK.sln @@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CONTRIBUTING.md = CONTRIBUTING.md Directory.Build.props = Directory.Build.props global.json = global.json + LICENSE.txt = LICENSE.txt README.md = README.md SECURITY.md = SECURITY.md EndProjectSection @@ -82,6 +83,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "setup-dotnet", "setup-dotne .github\actions\setup-dotnet\action.yml = .github\actions\setup-dotnet\action.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tableau.Migration.PythonGenerator", "src\Tableau.Migration.PythonGenerator\Tableau.Migration.PythonGenerator.csproj", "{F20029C7-4514-4668-8941-B2C3BC245CCB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -124,6 +127,10 @@ Global {99DA12FB-BB16-4EE1-9C9C-047755210255}.Debug|Any CPU.Build.0 = Debug|Any CPU {99DA12FB-BB16-4EE1-9C9C-047755210255}.Release|Any CPU.ActiveCfg = Release|Any CPU {99DA12FB-BB16-4EE1-9C9C-047755210255}.Release|Any CPU.Build.0 = Release|Any CPU + {F20029C7-4514-4668-8941-B2C3BC245CCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F20029C7-4514-4668-8941-B2C3BC245CCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F20029C7-4514-4668-8941-B2C3BC245CCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F20029C7-4514-4668-8941-B2C3BC245CCB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/examples/Csharp.ExampleApplication/Hooks/Mappings/ChangeProjectMapping.cs b/examples/Csharp.ExampleApplication/Hooks/Mappings/ChangeProjectMapping.cs index 8593aa5..6ca4e66 100644 --- a/examples/Csharp.ExampleApplication/Hooks/Mappings/ChangeProjectMapping.cs +++ b/examples/Csharp.ExampleApplication/Hooks/Mappings/ChangeProjectMapping.cs @@ -5,23 +5,24 @@ using Tableau.Migration; using Tableau.Migration.Content; using Tableau.Migration.Engine.Hooks.Mappings; +using Tableau.Migration.Resources; namespace Csharp.ExampleApplication.Hooks.Mappings { #region class - public class ChangeProjectMapping : IContentMapping, IContentMapping + public class ChangeProjectMapping : ContentMappingBase + where T : IContentReference, IMappableContainerContent { private static readonly StringComparer StringComparer = StringComparer.OrdinalIgnoreCase; - private readonly ILogger _logger; + private readonly ILogger>? _logger; - public ChangeProjectMapping(ILogger logger) + public ChangeProjectMapping(ISharedResourcesLocalizer? localizer, ILogger>? logger) : base(localizer, logger) { _logger = logger; } - private async Task?> ExecuteAsync(ContentMappingContext ctx) - where T : IContentReference, IMappableContainerContent + public override Task?> MapAsync(ContentMappingContext ctx, CancellationToken cancel) { // Get the container (project) location for the content item. var containerLocation = ctx.ContentItem.Location.Parent(); @@ -29,7 +30,7 @@ public ChangeProjectMapping(ILogger logger) // We only want to map content items whose project name is "Test". if (!StringComparer.Equals("Test", containerLocation.Name)) { - return ctx; + return ctx.ToTask(); } // Build the new project location. @@ -41,20 +42,20 @@ public ChangeProjectMapping(ILogger logger) // Map the new content item location. ctx = ctx.MapTo(newLocation); - _logger.LogInformation( + _logger?.LogInformation( "{ContentType} mapped from {OldLocation} to {NewLocation}.", typeof(T).Name, ctx.ContentItem.Location, ctx.MappedLocation); - return await ctx.ToTask(); + return ctx.ToTask(); } - public async Task?> ExecuteAsync(ContentMappingContext ctx, CancellationToken cancel) - => await ExecuteAsync(ctx); + public async Task?> MapAsync(ContentMappingContext ctx, CancellationToken cancel) + => await MapAsync(ctx, cancel); - public async Task?> ExecuteAsync(ContentMappingContext ctx, CancellationToken cancel) - => await ExecuteAsync(ctx); + public async Task?> MapAsync(ContentMappingContext ctx, CancellationToken cancel) + => await MapAsync(ctx, cancel); } #endregion } diff --git a/examples/Csharp.ExampleApplication/Hooks/Mappings/EmailDomainMapping.cs b/examples/Csharp.ExampleApplication/Hooks/Mappings/EmailDomainMapping.cs index 6c8441a..710be68 100644 --- a/examples/Csharp.ExampleApplication/Hooks/Mappings/EmailDomainMapping.cs +++ b/examples/Csharp.ExampleApplication/Hooks/Mappings/EmailDomainMapping.cs @@ -42,11 +42,12 @@ public class EmailDomainMapping : public override Task?> MapAsync(ContentMappingContext userMappingContext, CancellationToken cancel) { var domain = userMappingContext.MappedLocation.Parent(); + // Re-use an existing email if it already exists. if (!string.IsNullOrEmpty(userMappingContext.ContentItem.Email)) return userMappingContext.MapTo(domain.Append(userMappingContext.ContentItem.Email)).ToTask(); - // Takes the existing username and appends the default domain to build the email + // Takes the existing username and appends the domain to build the email var testEmail = $"{userMappingContext.ContentItem.Name}@{_domain}"; return userMappingContext.MapTo(domain.Append(testEmail)).ToTask(); } diff --git a/examples/Csharp.ExampleApplication/Hooks/Transformers/EncryptExtractTransformer.cs b/examples/Csharp.ExampleApplication/Hooks/Transformers/EncryptExtractTransformer.cs new file mode 100644 index 0000000..855df7c --- /dev/null +++ b/examples/Csharp.ExampleApplication/Hooks/Transformers/EncryptExtractTransformer.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tableau.Migration; +using Tableau.Migration.Content; +using Tableau.Migration.Engine.Hooks.Transformers; +using Tableau.Migration.Resources; + +namespace Csharp.ExampleApplication.Hooks.Transformers +{ + #region class + public class EncryptExtractsTransformer : ContentTransformerBase where T : IContentReference, IFileContent, IExtractContent + { + private readonly ILogger>? _logger; + + public EncryptExtractsTransformer(ISharedResourcesLocalizer? localizer, ILogger>? logger) : base(localizer, logger) + { + _logger = logger; + } + + public override async Task TransformAsync(T itemToTransform, CancellationToken cancel) + { + itemToTransform.EncryptExtracts = true; + + _logger?.LogInformation( + @"Setting encrypt extract to true for {ContentType} {ContentLocation}", + typeof(T).Name, + itemToTransform.Location); + + return await Task.FromResult(itemToTransform); + } + + public async Task TransformAsync(IPublishableWorkbook ctx, CancellationToken cancel) + => await TransformAsync(ctx, cancel); + + public async Task TransformAsync(IPublishableDataSource ctx, CancellationToken cancel) + => await TransformAsync(ctx, cancel); + } + #endregion +} diff --git a/examples/Csharp.ExampleApplication/Hooks/Transformers/MigratedTagTransformer.cs b/examples/Csharp.ExampleApplication/Hooks/Transformers/MigratedTagTransformer.cs index 701ee92..f48beff 100644 --- a/examples/Csharp.ExampleApplication/Hooks/Transformers/MigratedTagTransformer.cs +++ b/examples/Csharp.ExampleApplication/Hooks/Transformers/MigratedTagTransformer.cs @@ -6,41 +6,41 @@ using Tableau.Migration; using Tableau.Migration.Content; using Tableau.Migration.Engine.Hooks.Transformers; +using Tableau.Migration.Resources; namespace Csharp.ExampleApplication.Hooks.Transformers { #region class - public class MigratedTagTransformer : IContentTransformer, IContentTransformer + public class MigratedTagTransformer : ContentTransformerBase where T : IContentReference, IWithTags { - private readonly ILogger _logger; + private readonly ILogger>? _logger; - public MigratedTagTransformer(ILogger logger) + public MigratedTagTransformer(ISharedResourcesLocalizer? localizer, ILogger>? logger) : base(localizer, logger) { _logger = logger; } - protected async Task ExecuteAsync(T ctx) - where T : IContentReference, IWithTags + public override async Task TransformAsync(T itemToTransform, CancellationToken cancel) { var tag = "Migrated"; // Add the tag to the content item. - ctx.Tags.Add(new Tag(tag)); + itemToTransform.Tags.Add(new Tag(tag)); - _logger.LogInformation( + _logger?.LogInformation( @"Added ""{Tag}"" tag to {ContentType} {ContentLocation}.", tag, typeof(T).Name, - ctx.Location); + itemToTransform.Location); - return await Task.FromResult(ctx); + return await Task.FromResult(itemToTransform); } - public async Task ExecuteAsync(IPublishableWorkbook ctx, CancellationToken cancel) - => await ExecuteAsync(ctx); + public async Task TransformAsync(IPublishableWorkbook ctx, CancellationToken cancel) + => await TransformAsync(ctx, cancel); - public async Task ExecuteAsync(IPublishableDataSource ctx, CancellationToken cancel) - => await ExecuteAsync(ctx); + public async Task TransformAsync(IPublishableDataSource ctx, CancellationToken cancel) + => await TransformAsync(ctx, cancel); } #endregion } diff --git a/examples/Csharp.ExampleApplication/MyMigrationApplication.cs b/examples/Csharp.ExampleApplication/MyMigrationApplication.cs index f337e0d..1746c79 100644 --- a/examples/Csharp.ExampleApplication/MyMigrationApplication.cs +++ b/examples/Csharp.ExampleApplication/MyMigrationApplication.cs @@ -86,8 +86,8 @@ public async Task StartAsync(CancellationToken cancel) #endregion #region ChangeProjectMapping-Registration - _planBuilder.Mappings.Add(); - _planBuilder.Mappings.Add(); + _planBuilder.Mappings.Add, IDataSource>(); + _planBuilder.Mappings.Add, IWorkbook>(); #endregion // Add filters @@ -111,8 +111,13 @@ public async Task StartAsync(CancellationToken cancel) // Add transformers #region MigratedTagTransformer-Registration - _planBuilder.Transformers.Add(); - _planBuilder.Transformers.Add(); + _planBuilder.Transformers.Add, IPublishableDataSource>(); + _planBuilder.Transformers.Add, IPublishableWorkbook>(); + #endregion + + #region EncryptExtractTransformer-Registration + _planBuilder.Transformers.Add, IPublishableDataSource>(); + _planBuilder.Transformers.Add, IPublishableWorkbook>(); #endregion // Add migration action completed hooks diff --git a/examples/Csharp.ExampleApplication/Program.cs b/examples/Csharp.ExampleApplication/Program.cs index 8803e19..e524e05 100644 --- a/examples/Csharp.ExampleApplication/Program.cs +++ b/examples/Csharp.ExampleApplication/Program.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Tableau.Migration; +using Tableau.Migration.Content; #region namespace namespace Csharp.ExampleApplication @@ -56,7 +57,8 @@ public static IServiceCollection AddCustomizations(this IServiceCollection servi #endregion #region ChangeProjectMapping-DI - services.AddScoped(); + services.AddScoped>(); + services.AddScoped>(); #endregion #region DefaultProjectsFilter-DI @@ -76,7 +78,13 @@ public static IServiceCollection AddCustomizations(this IServiceCollection servi #endregion #region MigratedTagTransformer-DI - services.AddScoped(); + services.AddScoped>(); + services.AddScoped>(); + #endregion + + #region EncryptExtractTransformer-DI + services.AddScoped>(); + services.AddScoped>(); #endregion #region LogMigrationActionsHook-DI diff --git a/examples/Python.ExampleApplication/Hooks/Filters/default_project_filter.py b/examples/Python.ExampleApplication/Hooks/Filters/default_project_filter.py new file mode 100644 index 0000000..803d863 --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/Filters/default_project_filter.py @@ -0,0 +1,11 @@ +from tableau_migration import ( + IProject, + ContentMigrationItem, + ContentFilterBase) + + +class DefaultProjectFilter(ContentFilterBase[IProject]): + def should_migrate(self, item: ContentMigrationItem[IProject]) -> bool: + if item.source_item.name.casefold() == 'Default'.casefold(): + return False + return True \ No newline at end of file diff --git a/examples/Python.ExampleApplication/Hooks/Filters/unlicensed_user_filter.py b/examples/Python.ExampleApplication/Hooks/Filters/unlicensed_user_filter.py new file mode 100644 index 0000000..2a02ab3 --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/Filters/unlicensed_user_filter.py @@ -0,0 +1,12 @@ +from tableau_migration import ( + IUser, + ContentMigrationItem, + ContentFilterBase, + SiteRoles) + + +class UnlicensedUserFilter(ContentFilterBase[IUser]): + def should_migrate(self, item: ContentMigrationItem[IUser]) -> bool: + if item.source_item.license_level.casefold() == SiteRoles.UNLICENSED.casefold(): + return False + return True \ No newline at end of file diff --git a/examples/Python.ExampleApplication/Hooks/batch_migration_completed/log_migration_batches_hook.py b/examples/Python.ExampleApplication/Hooks/batch_migration_completed/log_migration_batches_hook.py new file mode 100644 index 0000000..3a5bf53 --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/batch_migration_completed/log_migration_batches_hook.py @@ -0,0 +1,34 @@ +import logging +from typing import TypeVar +from tableau_migration import( + ContentBatchMigrationCompletedHookBase, + IContentBatchMigrationResult, + IUser + ) + +T = TypeVar("T") + +class LogMigrationBatchesHook(ContentBatchMigrationCompletedHookBase[T]): + def __init__(self) -> None: + super().__init__() + self._logger = logging.getLogger(__name__) + + def execute(self, ctx: IContentBatchMigrationResult[T]) -> IContentBatchMigrationResult[T]: + + item_status = "" + for item in ctx.item_results: + item_status += "%s: %s".format(item.manifest_entry.source.location, item.manifest_entry.status) + + self._logger.info("%s batch of %d item(s) completed:\n%s", ctx._content_type, ctx.item_results.count, item_status) + + pass + +class LogMigrationBatchesHookForUsers(ContentBatchMigrationCompletedHookBase[IUser]): + def __init__(self) -> None: + super().__init__() + self._content_type = "User"; + +class LogMigrationBatchesHookForGoups(ContentBatchMigrationCompletedHookBase[IUser]): + def __init__(self) -> None: + super().__init__() + self._content_type = "Group"; \ No newline at end of file diff --git a/examples/Python.ExampleApplication/Hooks/mappings/change_project_mapping.py b/examples/Python.ExampleApplication/Hooks/mappings/change_project_mapping.py new file mode 100644 index 0000000..fe8f18f --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/mappings/change_project_mapping.py @@ -0,0 +1,38 @@ +from typing import TypeVar +from tableau_migration import( + IWorkbook, + IDataSource, + ContentMappingContext, + ContentMappingBase) + +T = TypeVar("T") + +class ChangeProjectMapping(ContentMappingBase[T]): + + def map(self, ctx: ContentMappingContext[T]) -> ContentMappingContext[T]: + # Get the container (project) location for the content item. + container_location = ctx.content_item.location.parent() + + # We only want to map content items whose project name is "Test". + if not container_location.name.casefold() == "Test".casefold(): + return ctx + + # Build the new project location. + new_container_location = container_location.rename("Production") + + # Build the new content item location. + new_location = new_container_location.append(ctx.content_item.name) + + # Map the new content item location. + ctx = ctx.map_to(new_location) + + return ctx + + +# Create the workbook version of the templated ChangeProjectMapping class +class ChangeProjectMappingForWorkbooks(ChangeProjectMapping[IWorkbook]): + pass + +# Create the datasource version of the templated ChangeProjectMapping class +class ChangeProjectMappingForDataSources(ChangeProjectMapping[IDataSource]): + pass \ No newline at end of file diff --git a/examples/Python.ExampleApplication/Hooks/mappings/email_domain_mapping.py b/examples/Python.ExampleApplication/Hooks/mappings/email_domain_mapping.py new file mode 100644 index 0000000..24c3a82 --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/mappings/email_domain_mapping.py @@ -0,0 +1,19 @@ +from tableau_migration import( + IUser, + TableauCloudUsernameMappingBase, + ContentMappingContext) + + +class EmailDomainMapping(TableauCloudUsernameMappingBase): + def map(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]: + _email_domain: str = "@mycompany.com" + + _tableau_user_domain = ctx.mapped_location.parent() + + # Re-use an existing email if it already exists. + if not ctx.content_item.email: + return ctx.map_to(_tableau_user_domain.append(ctx.content_item.email)) + + # Takes the existing username and appends the domain to build the email + new_email = ctx.content_item.name + _email_domain + return ctx.map_to(_tableau_user_domain.append(new_email)) \ No newline at end of file diff --git a/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py b/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py new file mode 100644 index 0000000..5607abb --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/mappings/project_rename_mapping.py @@ -0,0 +1,16 @@ +from tableau_migration import( + IProject, + ContentMappingBase, + ContentMappingContext) + + +class ProjectRenameMapping(ContentMappingBase[IProject]): + def map(self, ctx: ContentMappingContext[IProject]) -> ContentMappingContext[IProject]: + if not ctx.content_item.name.casefold() == "Test".casefold(): + return ctx + + new_location = ctx.content_item.location.rename("Production") + + ctx.map_to(new_location) + + return ctx \ No newline at end of file diff --git a/examples/Python.ExampleApplication/Hooks/migration_action_completed/log_migration_actions_hook.py b/examples/Python.ExampleApplication/Hooks/migration_action_completed/log_migration_actions_hook.py new file mode 100644 index 0000000..968852c --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/migration_action_completed/log_migration_actions_hook.py @@ -0,0 +1,22 @@ +import logging +from tableau_migration import( + MigrationActionCompletedHookBase, + IMigrationActionResult + ) + + +class LogMigrationActionsHook(MigrationActionCompletedHookBase): + def __init__(self) -> None: + super().__init__() + + # Create a logger for this class + self._logger = logging.getLogger(__name__) + + def execute(self, ctx: IMigrationActionResult) -> IMigrationActionResult: + if(ctx.success): + self._logger.info("Migration action completed successfully.") + else: + all_errors = "\n".join(ctx.errors) + self._logger.warning("Migration action completed with errors:\n%s", all_errors) + + return None diff --git a/examples/Python.ExampleApplication/Hooks/post_publish/bulk_logging_hook.py b/examples/Python.ExampleApplication/Hooks/post_publish/bulk_logging_hook.py new file mode 100644 index 0000000..d228114 --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/post_publish/bulk_logging_hook.py @@ -0,0 +1,19 @@ +import logging +from tableau_migration import ( + BulkPostPublishHookBase, + BulkPostPublishContext, + IDataSource) + + +class BulkLoggingHookForDataSources(BulkPostPublishHookBase[IDataSource]): + def __init__(self) -> None: + super().__init__() + + # Create a logger for this class + self._logger = logging.getLogger(__name__) + + def execute(self, ctx: BulkPostPublishContext[IDataSource]) -> BulkPostPublishContext[IDataSource]: + # Log the number of items published in the batch. + self._logger.info("Published %d IDataSource item(s).", ctx.published_items.count) + return None + \ No newline at end of file diff --git a/examples/Python.ExampleApplication/Hooks/transformers/encrypt_extracts_transformer.py b/examples/Python.ExampleApplication/Hooks/transformers/encrypt_extracts_transformer.py new file mode 100644 index 0000000..c0d1339 --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/transformers/encrypt_extracts_transformer.py @@ -0,0 +1,19 @@ +from typing import TypeVar +from tableau_migration import ( + ContentTransformerBase, + IPublishableWorkbook, + IPublishableDataSource) + +T = TypeVar("T") + +class EncryptExtractTransformer(ContentTransformerBase[T]): + def transform(self, itemToTransform: T) -> T: + itemToTransform.encrypt_extracts = True + + return itemToTransform + +class EncryptExtractTransformerForDataSources(EncryptExtractTransformer[IPublishableDataSource]): + pass + +class EncryptExtractTransformerForWorkbooks(EncryptExtractTransformer[IPublishableWorkbook]): + pass \ No newline at end of file diff --git a/examples/Python.ExampleApplication/Hooks/transformers/migrated_tag_transformer.py b/examples/Python.ExampleApplication/Hooks/transformers/migrated_tag_transformer.py new file mode 100644 index 0000000..f51418a --- /dev/null +++ b/examples/Python.ExampleApplication/Hooks/transformers/migrated_tag_transformer.py @@ -0,0 +1,23 @@ +from typing import TypeVar +from tableau_migration import ( + ContentTransformerBase, + IDataSource, + ITag, + IWorkbook) + +T = TypeVar("T") + + +class MigratedTagTransformer(ContentTransformerBase[T]): + def transform(self, itemToTransform: T) -> T: + tag: str = "Migrated" + + itemToTransform.tags.append(ITag(tag)) + + return itemToTransform + +class MigratedTagTransformerForDataSources(MigratedTagTransformer[IDataSource]): + pass + +class MigratedTagTransformerForWorkbooks(MigratedTagTransformer[IWorkbook]): + pass \ No newline at end of file diff --git a/examples/Python.ExampleApplication/Python.ExampleApplication.py b/examples/Python.ExampleApplication/Python.ExampleApplication.py index 4850fe8..a98fcd4 100644 --- a/examples/Python.ExampleApplication/Python.ExampleApplication.py +++ b/examples/Python.ExampleApplication/Python.ExampleApplication.py @@ -6,6 +6,7 @@ import os # environment variables import sys # system utility import tableau_migration # Tableau Migration SDK +import print_result from threading import Thread # threading @@ -32,7 +33,7 @@ def migrate(): access_token = os.environ.get('TABLEAU_MIGRATION_DESTINATION_TOKEN', config['DESTINATION']['ACCESS_TOKEN'])) \ .for_server_to_cloud() \ .with_tableau_id_authentication_type() \ - .with_tableau_cloud_usernames(config['USERS']['EMAIL_DOMAIN']) + .with_tableau_cloud_usernames(config['USERS']['EMAIL_DOMAIN']) # TODO: add filters, mappings, transformers, etc. here. @@ -47,6 +48,7 @@ def migrate(): results = migration.execute(plan) # TODO: Handle results here. + print_result(results) print("All done.") diff --git a/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj b/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj index 3f9cb7f..0af8ded 100644 --- a/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj +++ b/examples/Python.ExampleApplication/Python.ExampleApplication.pyproj @@ -5,8 +5,7 @@ 9c94fbc9-ae67-4a26-bdda-eb2ce9fe5c25 . Python.ExampleApplication.py - - + ..\..\src\Python\dist . . Python.ExampleApplication @@ -23,8 +22,17 @@ - - + + + + + + + + + + + @@ -43,6 +51,15 @@ X64 + + + + + + + + + + + + + +""" + +_expected_twb = """ + + + + + + + +""" + +def _transform_xml_content(xml: ElementTree.Element) -> None: + xml.find("user:test", {"user": "http://www.tableausoftware.com/xml/user"}).tail = "\n " + sub = ElementTree.SubElement(xml, "test2", { "a": "b" }) + sub.tail = "\n" + +class PyWorkbookXmlTransformer(PyXmlTransformer[PyPublishableWorkbook]): + + def needs_xml_transforming(self, ctx: PyPublishableWorkbook) -> bool: + return ctx.description == "mark" + + def transform(self, ctx: PyPublishableWorkbook, xml: ElementTree.Element) -> None: + ctx.description = xml.get("version") + _transform_xml_content(xml) + +def transform_workbook_xml(ctx: PyPublishableWorkbook, xml: ElementTree.Element) -> None: + ctx.description = xml.get("version") + _transform_xml_content(xml) + +def transform_workbook_xml_services(ctx: PyPublishableWorkbook, xml: ElementTree.Element, services: ScopedMigrationServices) -> None: + ctx.description = xml.get("version") + _transform_xml_content(xml) + +class TestXmlTransformerInterop(AutoFixtureTestBase): + + def _clean_xml_text(self, xml_text: str) -> str: + return xml_text.replace("\r", "") + + # Helper method to save XDocument to string that includes the XML declaration. + def _save_xml(self, xdoc: XDocument) -> str: + stream = MemoryStream() + writer = XmlWriter.Create(stream) + xdoc.Save(writer) + writer.Flush() + stream.Position = 0 + reader = StreamReader(stream) + return self._clean_xml_text(reader.ReadToEnd()) + + def test_transformer_interop_class(self): + hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) + + result = hook_builder.add(PyWorkbookXmlTransformer) + assert result is hook_builder + + hook_factories = hook_builder.build().get_hooks(IContentTransformer[IPublishableWorkbook]) + assert len(hook_factories) == 1 + + services = self.create(IServiceProvider) + ctx = self.create(IPublishableWorkbook) + xml = XDocument.Parse(_test_twb, LoadOptions.PreserveWhitespace) + + hook = hook_factories[0].Create[IXmlContentTransformer[IPublishableWorkbook]](services) + hook.TransformAsync(ctx, xml, CancellationToken(False)).GetAwaiter().GetResult() + + assert ctx.Description == "18.1" + + saved_xml = self._save_xml(xml) + assert saved_xml == self._clean_xml_text(_expected_twb) + + def test_transformer_needs_transforming(self): + hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) + + result = hook_builder.add(PyWorkbookXmlTransformer) + assert result is hook_builder + + hook_factories = hook_builder.build().get_hooks(IContentTransformer[IPublishableWorkbook]) + assert len(hook_factories) == 1 + + services = self.create(IServiceProvider) + ctx = self.create(IPublishableWorkbook) + + hook = hook_factories[0].Create[IXmlContentTransformer[IPublishableWorkbook]](services) + + ctx.Description = "notmark" + + assert hook.NeedsXmlTransforming(ctx) == False + + ctx.Description = "mark" + + assert hook.NeedsXmlTransforming(ctx) == True + + def test_transformer_interop_callback(self): + hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) + + ctx = self.create(IPublishableWorkbook) + xml = XDocument.Parse(_test_twb, LoadOptions.PreserveWhitespace) + + result = hook_builder.add(PyPublishableWorkbook, transform_workbook_xml, is_xml = True) + assert result is hook_builder + + hook_factories = hook_builder.build().get_hooks(IContentTransformer[IPublishableWorkbook]) + assert len(hook_factories) == 1 + + services = self.create(IServiceProvider) + + hook = hook_factories[0].Create[IXmlContentTransformer[IPublishableWorkbook]](services) + hook.TransformAsync(ctx, xml, CancellationToken(False)).GetAwaiter().GetResult() + + assert ctx.Description == "18.1" + assert self._save_xml(xml) == self._clean_xml_text(_expected_twb) + + def test_transformer_interop_callback_services(self): + hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) + + ctx = self.create(IPublishableWorkbook) + xml = XDocument.Parse(_test_twb, LoadOptions.PreserveWhitespace) + + result = hook_builder.add(PyPublishableWorkbook, transform_workbook_xml_services, is_xml = True) + assert result is hook_builder + + hook_factories = hook_builder.build().get_hooks(IContentTransformer[IPublishableWorkbook]) + assert len(hook_factories) == 1 + + services = self.create(IServiceProvider) + + hook = hook_factories[0].Create[IXmlContentTransformer[IPublishableWorkbook]](services) + hook.TransformAsync(ctx, xml, CancellationToken(False)).GetAwaiter().GetResult() + + assert ctx.Description == "18.1" + assert self._save_xml(xml) == self._clean_xml_text(_expected_twb) + \ No newline at end of file diff --git a/src/Python/tests/test_migration_engine_migrators_batch.py b/src/Python/tests/test_migration_engine_migrators_batch.py new file mode 100644 index 0000000..daeef4e --- /dev/null +++ b/src/Python/tests/test_migration_engine_migrators_batch.py @@ -0,0 +1,35 @@ +# Copyright (c) 2024, Salesforce, Inc. +# SPDX-License-Identifier: Apache-2 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import uuid + +from tableau_migration.migration_content import PyUser +from tableau_migration.migration_engine_migrators_batch import PyContentBatchMigrationResult +from tests.helpers.autofixture import AutoFixtureTestBase + +from Tableau.Migration.Content import IUser +from Tableau.Migration.Engine.Migrators.Batch import IContentBatchMigrationResult + +class TestPyContentBatchMigrationResult(AutoFixtureTestBase): + + def test_item_results(self): + dotnet = self.create(IContentBatchMigrationResult[IUser]) + + py = PyContentBatchMigrationResult[PyUser](dotnet) + + assert len(dotnet.ItemResults) != 0 + assert len(py.item_results) == len(dotnet.ItemResults) + assert py.item_results[0].manifest_entry.source.id == uuid.UUID(dotnet.ItemResults[0].ManifestEntry.Source.Id.ToString()) \ No newline at end of file diff --git a/src/Python/tests/test_migrations_engine_options.py b/src/Python/tests/test_migration_engine_options.py similarity index 95% rename from src/Python/tests/test_migrations_engine_options.py rename to src/Python/tests/test_migration_engine_options.py index 26b361b..c7166ce 100644 --- a/src/Python/tests/test_migrations_engine_options.py +++ b/src/Python/tests/test_migration_engine_options.py @@ -21,11 +21,13 @@ import pytest from tableau_migration.migration import ( get_service_provider, - get_service ) + get_service +) from tableau_migration.migration_engine_options import ( PyMigrationPlanOptionsBuilder, - PyMigrationPlanOptionsCollection) + PyMigrationPlanOptionsCollection +) import System @@ -35,7 +37,8 @@ from Tableau.Migration.Engine.Options import ( IMigrationPlanOptionsBuilder, - IMigrationPlanOptionsCollection) + IMigrationPlanOptionsCollection +) import Moq _dist_path = abspath(Path(__file__).parent.resolve().__str__() + "/../src/tableau_migration") diff --git a/src/Python/tests/test_migration_services.py b/src/Python/tests/test_migration_services.py new file mode 100644 index 0000000..a02354f --- /dev/null +++ b/src/Python/tests/test_migration_services.py @@ -0,0 +1,112 @@ +# Copyright (c) 2024, Salesforce, Inc. +# SPDX-License-Identifier: Apache-2 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from System import IServiceProvider + +from tableau_migration.migration import ( + PyMigrationManifest +) +from tableau_migration.migration_content import ( + PyUser, + PyGroup, + PyProject, + PyDataSource, + PyWorkbook +) +from tableau_migration.migration_engine import ( + PyMigrationPlan +) + +from tableau_migration.migration_engine_endpoints_search import ( + PyDestinationContentReferenceFinderFactory, + PyDestinationContentReferenceFinder, + PySourceContentReferenceFinderFactory, + PySourceContentReferenceFinder +) + +from tableau_migration.migration_services import ( + ScopedMigrationServices +) + +from tests.helpers.autofixture import AutoFixtureTestBase + +class TestPySourceContentReferenceFinder(AutoFixtureTestBase): + _test_data = [ + PyUser, + PyGroup, + PyProject, + PyDataSource, + PyWorkbook + ] + + def test_get_manifest(self): + dotnet_provider = self.create(IServiceProvider) + scoped_services = ScopedMigrationServices(dotnet_provider) + + result = scoped_services.get_manifest() + + assert result is not None + assert isinstance(result, PyMigrationManifest) + + def test_get_plan(self): + dotnet_provider = self.create(IServiceProvider) + scoped_services = ScopedMigrationServices(dotnet_provider) + + result = scoped_services.get_plan() + + assert result is not None + assert isinstance(result, PyMigrationPlan) + + def test_get_source_finder_factory(self): + dotnet_provider = self.create(IServiceProvider) + scoped_services = ScopedMigrationServices(dotnet_provider) + + result = scoped_services.get_source_finder_factory() + + assert result is not None + assert isinstance(result, PySourceContentReferenceFinderFactory) + + @pytest.mark.parametrize("searched_type", _test_data) + def test_get_source_finder(self, searched_type): + dotnet_provider = self.create(IServiceProvider) + scoped_services = ScopedMigrationServices(dotnet_provider) + + result = scoped_services.get_source_finder(searched_type) + + assert result is not None + assert isinstance(result, PySourceContentReferenceFinder) + assert result._content_type is searched_type + + def test_get_destination_finder_factory(self): + dotnet_provider = self.create(IServiceProvider) + scoped_services = ScopedMigrationServices(dotnet_provider) + + result = scoped_services.get_destination_finder_factory() + + assert result is not None + assert isinstance(result, PyDestinationContentReferenceFinderFactory) + + @pytest.mark.parametrize("searched_type", _test_data) + def test_get_destination_finder(self, searched_type): + dotnet_provider = self.create(IServiceProvider) + scoped_services = ScopedMigrationServices(dotnet_provider) + + result = scoped_services.get_destination_finder(searched_type) + + assert result is not None + assert isinstance(result, PyDestinationContentReferenceFinder) + assert result._content_type is searched_type diff --git a/src/Python/tests/test_migrations_engine_hooks.py b/src/Python/tests/test_migrations_engine_hooks.py deleted file mode 100644 index 7dda9ed..0000000 --- a/src/Python/tests/test_migrations_engine_hooks.py +++ /dev/null @@ -1,272 +0,0 @@ -# Copyright (c) 2024, Salesforce, Inc. -# SPDX-License-Identifier: Apache-2 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Make sure the test can find the module -from tableau_migration.migration import get_service -from tableau_migration.migration_engine_hooks import PyMigrationHookBuilder -from System import IServiceProvider, Func, ArgumentException, NotImplementedException -from System.Threading import CancellationToken -from Microsoft.Extensions.DependencyInjection import ( - ServiceCollection, - ServiceCollectionContainerBuilderExtensions) -from Tableau.Migration.Interop.Hooks import ISyncMigrationHook -from Tableau.Migration.Engine.Hooks import MigrationHookBuilder, IMigrationHook - -class ClassImplementation(ISyncMigrationHook[bool]): - __namespace__ = "Tableau.Migration.Custom.Hooks" - - def Execute(self, ctx) -> bool: - return not ctx - -class SubClassImplementation(ClassImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks" - - def Execute(self, ctx) -> bool: - return not ctx - -class SubSubClassImplementation(SubClassImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks" - - def Execute(self, ctx) -> bool: - return not ctx - -class RawHook(IMigrationHook[str]): - __namespace__ = "Tableau.Migration.Custom.Hooks" - - def Execute(self, ctx) -> str: - return ctx - -class WithoutImplementation(ISyncMigrationHook[bool]): - __namespace__ = "Tableau.Migration.Custom.Hooks" - -class TestMigrationHookBuilderTests(): - def test_clear_class_hook(self): - hook_builder = PyMigrationHookBuilder(MigrationHookBuilder()) - - result = hook_builder.add(ClassImplementation()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncMigrationHook[bool])) == 0 - - def test_clear_subclass_hook(self): - hook_builder = PyMigrationHookBuilder(MigrationHookBuilder()) - - result = hook_builder.add(SubClassImplementation()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncMigrationHook[int])) == 0 - - def test_clear_subsubclass_hook(self): - hook_builder = PyMigrationHookBuilder(MigrationHookBuilder()) - - result = hook_builder.add(SubSubClassImplementation()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncMigrationHook[int])) == 0 - - def test_add_object(self): - hook_builder = PyMigrationHookBuilder(MigrationHookBuilder()) - - result = hook_builder.add(SubSubClassImplementation()) - - assert result is hook_builder - - def test_add_type_noinitializer(self): - hook_builder = PyMigrationHookBuilder(MigrationHookBuilder()) - - result = hook_builder.add(SubSubClassImplementation) - - assert result is hook_builder - - def test_add_type_initializer(self): - hook_builder = PyMigrationHookBuilder(MigrationHookBuilder()) - - def subclassinitialize(provider: IServiceProvider): - return get_service(provider, SubClassImplementation) - - result = hook_builder.add(SubClassImplementation, Func[IServiceProvider, SubClassImplementation](subclassinitialize)) - - assert result is hook_builder - - def test_add_callback(self): - hook_builder = PyMigrationHookBuilder(MigrationHookBuilder()) - - def classcallback(context: str): - return context - - result = hook_builder.add(ISyncMigrationHook[str], str, Func[str, str](classcallback)) - - assert result is hook_builder - - def test_add_ignores_raw_hook_interface(self): - try: - PyMigrationHookBuilder(MigrationHookBuilder()).add(RawHook()) - except ArgumentException as ae: - assert ae.args[0] == "Type Tableau.Migration.Custom.Hooks.RawHook does not implement any migration hook types." - else: - assert False, "This test must generate an ArgumentException." - - def test_add_withoutimplementation(self): - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(WithoutImplementation()).build().get_hooks(ISyncMigrationHook[bool]) - assert len(hookFactory) == 1 - hook = hookFactory[0].Create[IMigrationHook[bool]](provider) - try: - hook.ExecuteAsync(False, CancellationToken(False)) - except NotImplementedException as nie: - assert nie.args[0] == "Python object does not have a 'Execute' method" - else: - assert False, "This test must generate an NotImplementedException." - finally: - provider.Dispose() - - def test_add_withimplementation(self): - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(SubClassImplementation()).build().get_hooks(ISyncMigrationHook[bool]) - assert len(hookFactory) == 1 - hook = hookFactory[0].Create[IMigrationHook[bool]](provider) - try: - result = hook.ExecuteAsync(False, CancellationToken(False)) - - assert result - except: - assert False, "This test must not generate an Exception." - finally: - provider.Dispose() - - def test_build_detects_interface_from_concrete_class(self): - collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(ClassImplementation()).build() - hooks = collection.get_hooks(ISyncMigrationHook[bool]) - internalhooks = collection.get_hooks(IMigrationHook[bool]) - otherhooks = collection.get_hooks(ISyncMigrationHook[int]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - subsubclasshooks = collection.get_hooks(SubSubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - assert len(subsubclasshooks) == 0 - - def test_build_detects_interface_from_concrete_subclass(self): - collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(SubClassImplementation()).build() - hooks = collection.get_hooks(ISyncMigrationHook[bool]) - internalhooks = collection.get_hooks(IMigrationHook[bool]) - otherhooks = collection.get_hooks(ISyncMigrationHook[int]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - subsubclasshooks = collection.get_hooks(SubSubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - assert len(subsubclasshooks) == 0 - - def test_build_detects_interface_from_concrete_subsubclass(self): - collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(SubSubClassImplementation()).build() - hooks = collection.get_hooks(ISyncMigrationHook[bool]) - internalhooks = collection.get_hooks(IMigrationHook[bool]) - otherhooks = collection.get_hooks(ISyncMigrationHook[int]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - subsubclasshooks = collection.get_hooks(SubSubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - assert len(subsubclasshooks) == 0 - - def test_build_factory_class(self): - collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(ClassImplementation).build() - hooks = collection.get_hooks(ISyncMigrationHook[bool]) - internalhooks = collection.get_hooks(IMigrationHook[bool]) - otherhooks = collection.get_hooks(ISyncMigrationHook[int]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - subsubclasshooks = collection.get_hooks(SubSubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - assert len(subsubclasshooks) == 0 - - def test_build_factory_subclass_withinitializer(self): - def subclassinitialize(provider: IServiceProvider): - return get_service(provider, SubClassImplementation) - - collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(SubClassImplementation, Func[IServiceProvider, SubClassImplementation](subclassinitialize)).build() - hooks = collection.get_hooks(ISyncMigrationHook[bool]) - internalhooks = collection.get_hooks(IMigrationHook[bool]) - otherhooks = collection.get_hooks(ISyncMigrationHook[int]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - subsubclasshooks = collection.get_hooks(SubSubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - assert len(subsubclasshooks) == 0 - - def test_build_factory_multipleclasses(self): - collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(ClassImplementation).add(SubClassImplementation).add(SubSubClassImplementation).build() - hooks = collection.get_hooks(ISyncMigrationHook[bool]) - internalhooks = collection.get_hooks(IMigrationHook[bool]) - otherhooks = collection.get_hooks(ISyncMigrationHook[int]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - subsubclasshooks = collection.get_hooks(SubSubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 3 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - assert len(subsubclasshooks) == 0 - - def test_build_callback(self): - def classcallback(context: str): - return context - - collection = PyMigrationHookBuilder(MigrationHookBuilder()).add(ISyncMigrationHook[str], str, Func[str, str](classcallback)).build() - hooks = collection.get_hooks(ISyncMigrationHook[str]) - internalhooks = collection.get_hooks(IMigrationHook[str]) - otherhooks = collection.get_hooks(ISyncMigrationHook[int]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - subsubclasshooks = collection.get_hooks(SubSubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - assert len(subsubclasshooks) == 0 diff --git a/src/Python/tests/test_migrations_engine_hooks_filters.py b/src/Python/tests/test_migrations_engine_hooks_filters.py deleted file mode 100644 index 4afc1cc..0000000 --- a/src/Python/tests/test_migrations_engine_hooks_filters.py +++ /dev/null @@ -1,243 +0,0 @@ -# Copyright (c) 2024, Salesforce, Inc. -# SPDX-License-Identifier: Apache-2 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Make sure the test can find the module -from tableau_migration.migration import get_service -from tableau_migration.migration_engine_hooks_filters import PyContentFilterBuilder -from System import IServiceProvider, Func, ArgumentException, NotImplementedException -from System.Collections.Generic import IEnumerable, List -from System.Threading import CancellationToken -from Microsoft.Extensions.DependencyInjection import ( - ServiceCollection, - ServiceCollectionContainerBuilderExtensions) -from Tableau.Migration.Content import IUser,IProject -from Tableau.Migration.Engine import ContentMigrationItem -from Tableau.Migration.Engine.Hooks import IMigrationHook -from Tableau.Migration.Engine.Hooks.Filters import ContentFilterBuilder,IContentFilter,ContentFilterBase -from Tableau.Migration.Interop.Hooks.Filters import ISyncContentFilter - -class ClassImplementation(ISyncContentFilter[IUser]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Filters" - - def Execute(self, ctx: IEnumerable[ContentMigrationItem[IUser]]) -> IEnumerable[ContentMigrationItem[IUser]]: - return ctx - - -class SubClassImplementation(ClassImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks.Filters" - - def Execute(self, ctx: IEnumerable[ContentMigrationItem[IUser]]) -> IEnumerable[ContentMigrationItem[IUser]]: - print("In Execute of SubClassImplementation(ClassImplementation)" ) - return ctx - - -class RawHook(): - __namespace__ = "Tableau.Migration.Custom.Hooks.Filters" - - def ShouldMigrate(self, ctx) -> bool: - return True - - -class WithoutImplementation(ISyncContentFilter[IUser]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Filters" - - -class TestContentFilterBuilderTests(): - def test_clear_class_hook(self): - hook_builder = PyContentFilterBuilder(ContentFilterBuilder()) - - result = hook_builder.add(IUser,ClassImplementation()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncContentFilter[IUser])) == 0 - - def test_clear_subclass_hook(self): - hook_builder = PyContentFilterBuilder(ContentFilterBuilder()) - - result = hook_builder.add(IUser,SubClassImplementation()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncContentFilter[IUser])) == 0 - - def test_clear_raw_hook(self): - try: - hook_builder = PyContentFilterBuilder(ContentFilterBuilder()) - - result = hook_builder.add(IProject,RawHook()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncContentFilter[IUser])) == 0 - except Exception as e: - print(e); - - - def test_add_object(self): - hook_builder = PyContentFilterBuilder(ContentFilterBuilder()) - - result = hook_builder.add(IUser,SubClassImplementation()) - - assert result is hook_builder - - def test_add_type_noinitializer(self): - hook_builder = PyContentFilterBuilder(ContentFilterBuilder()) - - result = hook_builder.add(ClassImplementation,IUser) - - assert result is hook_builder - - def test_add_type_initializer(self): - hook_builder = PyContentFilterBuilder(ContentFilterBuilder()) - - def subclassinitialize(provider: IServiceProvider): - return get_service(provider, SubClassImplementation) - - result = hook_builder.add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize)) - - assert result is hook_builder - - def test_add_callback(self): - hook_builder = PyContentFilterBuilder(ContentFilterBuilder()) - - def classcallback(context: IEnumerable[ContentMigrationItem[IProject]]): - return context - - result = hook_builder.add(IProject, Func[IEnumerable[ContentMigrationItem[IProject]], IEnumerable[ContentMigrationItem[IProject]]](classcallback)) - - assert result is hook_builder - - def test_add_withoutimplementation(self): - users = List[ContentMigrationItem[IUser]](); - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser, WithoutImplementation()).build().get_hooks(IContentFilter[IUser]) - assert len(hookFactory) == 1 - hook = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]]](provider) - try: - hook.ExecuteAsync(users, CancellationToken(False)) - except NotImplementedException as nie: - assert nie.args[0] == "Python object does not have a 'Execute' method" - else: - assert False, "This test must generate a NotImplementedException." - finally: - provider.Dispose() - - def test_add_withimplementation(self): - users = List[ContentMigrationItem[IUser]](); - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser, SubClassImplementation()).build().get_hooks(IContentFilter[IUser]) - assert len(hookFactory) == 1 - hook = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]]](provider) - try: - result = hook.ExecuteAsync(users, CancellationToken(False)) - print(result) - except: - assert False, "This test must not generate an Exception." - finally: - provider.Dispose() - - def test_build_detects_interface_from_concrete_class(self): - collection = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser,ClassImplementation()).build() - hooks = collection.get_hooks(IContentFilter[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]]) - otherhooks = collection.get_hooks(IContentFilter[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_detects_interface_from_concrete_subclass(self): - collection = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser,SubClassImplementation()).build() - hooks = collection.get_hooks(IContentFilter[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]]) - otherhooks = collection.get_hooks(IContentFilter[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_factory_class(self): - collection = PyContentFilterBuilder(ContentFilterBuilder()).add(ClassImplementation,IUser).build() - hooks = collection.get_hooks(IContentFilter[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]]) - otherhooks = collection.get_hooks(IContentFilter[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_factory_subclass_withinitializer(self): - def subclassinitialize(provider: IServiceProvider): - return get_service(provider, SubClassImplementation) - - collection = PyContentFilterBuilder(ContentFilterBuilder()).add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize)).build() - hooks = collection.get_hooks(IContentFilter[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]]) - otherhooks = collection.get_hooks(IContentFilter[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_factory_multipleclasses(self): - collection = PyContentFilterBuilder(ContentFilterBuilder()).add(ClassImplementation,IUser).add(SubClassImplementation,IUser).build() - hooks = collection.get_hooks(IContentFilter[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IUser]]]) - otherhooks = collection.get_hooks(IContentFilter[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 2 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_callback(self): - def classcallback(context: IEnumerable[ContentMigrationItem[IProject]]): - return context - - collection = PyContentFilterBuilder(ContentFilterBuilder()).add(IProject, Func[IEnumerable[ContentMigrationItem[IProject]], IEnumerable[ContentMigrationItem[IProject]]](classcallback)).build() - hooks = collection.get_hooks(IContentFilter[IProject]) - internalhooks = collection.get_hooks(IMigrationHook[IEnumerable[ContentMigrationItem[IProject]]]) - otherhooks = collection.get_hooks(IContentFilter[IUser]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 diff --git a/src/Python/tests/test_migrations_engine_hooks_filters_lifetime.py b/src/Python/tests/test_migrations_engine_hooks_filters_lifetime.py deleted file mode 100644 index c5a046d..0000000 --- a/src/Python/tests/test_migrations_engine_hooks_filters_lifetime.py +++ /dev/null @@ -1,373 +0,0 @@ -# Copyright (c) 2024, Salesforce, Inc. -# SPDX-License-Identifier: Apache-2 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Make sure the test can find the module -import pytest -from tableau_migration.migration import get_service -from tableau_migration.migration_engine_hooks_filters import PyContentFilterBuilder -from System import IServiceProvider, Func -from Microsoft.Extensions.DependencyInjection import ( - ServiceProviderServiceExtensions, - ServiceCollectionServiceExtensions, - ServiceCollection, - ServiceCollectionContainerBuilderExtensions) -from System.Collections.Generic import IEnumerable -from Tableau.Migration.Content import IUser,IProject -from Tableau.Migration.Engine import ContentMigrationItem -from Tableau.Migration.Engine.Hooks import IMigrationHook -from Tableau.Migration.Engine.Hooks.Filters import ContentFilterBuilder,IContentFilter -from Tableau.Migration.Interop.Hooks.Filters import ISyncContentFilter - -class FilterImplementation(ISyncContentFilter[IUser]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Filters.Lifetime" - - def Execute(self, ctx: IEnumerable[ContentMigrationItem[IUser]]) -> IEnumerable[ContentMigrationItem[IUser]]: - return ctx - -class SubFilterImplementation(FilterImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks.Filters.Lifetime" - - def Execute(self, ctx: IEnumerable[ContentMigrationItem[IUser]]) -> IEnumerable[ContentMigrationItem[IUser]]: - return ctx - -class TestContentFilterBuilderLifetimeTests(): - @pytest.fixture(autouse=True, scope="class") - def setup(self): - TestContentFilterBuilderLifetimeTests.FilterImplementation = FilterImplementation - TestContentFilterBuilderLifetimeTests.SubFilterImplementation = SubFilterImplementation - - yield - - del TestContentFilterBuilderLifetimeTests.SubFilterImplementation - del TestContentFilterBuilderLifetimeTests.FilterImplementation - - def test_lifetime_buildandcreate_object(self, skip_by_python_lifetime_env_var): - try: - # Arrange - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hook = TestContentFilterBuilderLifetimeTests.SubFilterImplementation() - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(IUser,hook).build().get_hooks(IContentFilter[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert hook == firstScopeHook1 - assert hook is not firstScopeHook1 - assert hook == firstScopeHook2 - assert hook is not firstScopeHook2 - assert hook == lastScopeHook - assert hook is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializersingleton(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddSingleton[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation,IUser).build().get_hooks(IContentFilter[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializerscoped(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddScoped[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation,IUser).build().get_hooks(IContentFilter[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializertransient(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddTransient[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation,IUser).build().get_hooks(IContentFilter[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializerobjectreference(self, skip_by_python_lifetime_env_var): - try: - # Arrange - hook = TestContentFilterBuilderLifetimeTests.SubFilterImplementation() - def subsubclassinitialize(provider: IServiceProvider): - return hook - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.SubFilterImplementation](subsubclassinitialize)).build().get_hooks(IContentFilter[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializernewobject(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return TestContentFilterBuilderLifetimeTests.SubFilterImplementation() - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.SubFilterImplementation](subsubclassinitialize)).build().get_hooks(IContentFilter[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializersingleton(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return get_service(provider,TestContentFilterBuilderLifetimeTests.SubFilterImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddSingleton[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.SubFilterImplementation](subsubclassinitialize)).build().get_hooks(IContentFilter[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializerscoped(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def classinitialize(provider: IServiceProvider): - return get_service(provider,TestContentFilterBuilderLifetimeTests.FilterImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddScoped[TestContentFilterBuilderLifetimeTests.FilterImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.FilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.FilterImplementation](classinitialize)).build().get_hooks(IContentFilter[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializertransient(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subclassinitialize(provider: IServiceProvider): - return get_service(provider,TestContentFilterBuilderLifetimeTests.SubFilterImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddTransient[TestContentFilterBuilderLifetimeTests.SubFilterImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(TestContentFilterBuilderLifetimeTests.SubFilterImplementation, IUser, Func[IServiceProvider, TestContentFilterBuilderLifetimeTests.SubFilterImplementation](subclassinitialize)).build().get_hooks(IContentFilter[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentFilter[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentFilter[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_callback(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def classcallback(context: IEnumerable[ContentMigrationItem[IProject]]): - return context - - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentFilterBuilder(ContentFilterBuilder()).add(IProject, Func[IEnumerable[ContentMigrationItem[IProject]], IEnumerable[ContentMigrationItem[IProject]]](classcallback)).build().get_hooks(IContentFilter[IProject]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IProject]]]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IProject]]]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[IEnumerable[ContentMigrationItem[IProject]]]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() diff --git a/src/Python/tests/test_migrations_engine_hooks_lifetime.py b/src/Python/tests/test_migrations_engine_hooks_lifetime.py deleted file mode 100644 index 458bb89..0000000 --- a/src/Python/tests/test_migrations_engine_hooks_lifetime.py +++ /dev/null @@ -1,378 +0,0 @@ -# Copyright (c) 2024, Salesforce, Inc. -# SPDX-License-Identifier: Apache-2 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Make sure the test can find the module -import pytest -from tableau_migration.migration import get_service -from tableau_migration.migration_engine_hooks import PyMigrationHookBuilder -from System import IServiceProvider, Func -from Microsoft.Extensions.DependencyInjection import ( - ServiceProviderServiceExtensions, - ServiceCollectionServiceExtensions, - ServiceCollection, - ServiceCollectionContainerBuilderExtensions) -from Tableau.Migration.Engine.Hooks import MigrationHookBuilder, IMigrationHook -from Tableau.Migration.Interop.Hooks import ISyncMigrationHook - -class HookImplementation(ISyncMigrationHook[bool]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Lifetime" - - def Execute(self, ctx) -> bool: - return ctx - -class SubHookImplementation(HookImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks.Lifetime" - - def Execute(self, ctx) -> bool: - return ctx - -class SubSubHookImplementation(SubHookImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks.Lifetime" - - def Execute(self, ctx) -> bool: - return ctx - -class TestMigrationHookBuilderLifetimeTests(): - @pytest.fixture(autouse=True, scope="class") - def setup(self): - TestMigrationHookBuilderLifetimeTests.HookImplementation = HookImplementation - TestMigrationHookBuilderLifetimeTests.SubHookImplementation = SubHookImplementation - TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation = SubSubHookImplementation - - yield - - del TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation - del TestMigrationHookBuilderLifetimeTests.SubHookImplementation - del TestMigrationHookBuilderLifetimeTests.HookImplementation - - def test_lifetime_buildandcreate_object(self, skip_by_python_lifetime_env_var): - try: - # Arrange - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hook = TestMigrationHookBuilderLifetimeTests.SubHookImplementation() - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(hook).build().get_hooks(ISyncMigrationHook[bool]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert hook == firstScopeHook1 - assert hook is not firstScopeHook1 - assert hook == firstScopeHook2 - assert hook is not firstScopeHook2 - assert hook == lastScopeHook - assert hook is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializersingleton(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddSingleton[TestMigrationHookBuilderLifetimeTests.SubHookImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubHookImplementation).build().get_hooks(ISyncMigrationHook[bool]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializerscoped(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddScoped[TestMigrationHookBuilderLifetimeTests.SubHookImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubHookImplementation).build().get_hooks(ISyncMigrationHook[bool]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializertransient(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddTransient[TestMigrationHookBuilderLifetimeTests.SubHookImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubHookImplementation).build().get_hooks(ISyncMigrationHook[bool]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - - def test_lifetime_buildandcreate_initializerobjectreference(self, skip_by_python_lifetime_env_var): - try: - # Arrange - hook = TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation() - def subsubclassinitialize(provider: IServiceProvider): - return hook - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializernewobject(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation() - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializersingleton(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return get_service(provider,TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddSingleton[TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializerscoped(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return get_service(provider,TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddScoped[TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializertransient(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return get_service(provider,TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddTransient[TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation, Func[IServiceProvider, TestMigrationHookBuilderLifetimeTests.SubSubHookImplementation](subsubclassinitialize)).build().get_hooks(ISyncMigrationHook[bool]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[bool]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[bool]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_callback(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def classcallback(context: str): - return context - - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyMigrationHookBuilder(MigrationHookBuilder()).add(ISyncMigrationHook[str], str, Func[str, str](classcallback)).build().get_hooks(ISyncMigrationHook[str]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[str]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[str]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[str]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() diff --git a/src/Python/tests/test_migrations_engine_hooks_mappings.py b/src/Python/tests/test_migrations_engine_hooks_mappings.py deleted file mode 100644 index 26cd620..0000000 --- a/src/Python/tests/test_migrations_engine_hooks_mappings.py +++ /dev/null @@ -1,188 +0,0 @@ -# Copyright (c) 2024, Salesforce, Inc. -# SPDX-License-Identifier: Apache-2 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Make sure the test can find the module -from tableau_migration.migration import get_service -from tableau_migration.migration_engine_hooks_mappings import PyContentMappingBuilder -from System import IServiceProvider, Func, ArgumentException -from Tableau.Migration.Content import IUser, IProject -from Tableau.Migration.Engine.Hooks import IMigrationHook -from Tableau.Migration.Engine.Hooks.Mappings import ContentMappingBuilder, IContentMapping, ContentMappingBase, ContentMappingContext -from Tableau.Migration.Interop.Hooks.Mappings import ISyncContentMapping - -class ClassImplementation(ISyncContentMapping[IUser]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings" - - def Execute(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]: - return ctx - -class SubClassImplementation(ClassImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings" - - def Execute(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]: - return ctx - -class RawHook(ContentMappingBase[IProject]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings" - - def ShouldMigrate(self, ctx) -> bool: - return True - -class TestContentMappingBuilderTests(): - def test_clear_class_hook(self): - hook_builder = PyContentMappingBuilder(ContentMappingBuilder()) - - result = hook_builder.add(IUser,ClassImplementation()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncContentMapping[IUser])) == 0 - - def test_clear_subclass_hook(self): - hook_builder = PyContentMappingBuilder(ContentMappingBuilder()) - - result = hook_builder.add(IUser,SubClassImplementation()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncContentMapping[IUser])) == 0 - - def test_add_object(self): - hook_builder = PyContentMappingBuilder(ContentMappingBuilder()) - - result = hook_builder.add(IUser,SubClassImplementation()) - - assert result is hook_builder - - def test_add_type_noinitializer(self): - hook_builder = PyContentMappingBuilder(ContentMappingBuilder()) - - result = hook_builder.add(ClassImplementation,IUser) - - assert result is hook_builder - - def test_add_type_initializer(self): - hook_builder = PyContentMappingBuilder(ContentMappingBuilder()) - - def subclassinitialize(provider: IServiceProvider): - return get_service(provider, SubClassImplementation) - - result = hook_builder.add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize)) - - assert result is hook_builder - - def test_add_callback(self): - hook_builder = PyContentMappingBuilder(ContentMappingBuilder()) - - def classcallback(context: ContentMappingContext[IProject]): - return context - - result = hook_builder.add(IProject, Func[ContentMappingContext[IProject], ContentMappingContext[IProject]](classcallback)) - - assert result is hook_builder - - def test_build_detects_interface_from_concrete_class(self): - collection = PyContentMappingBuilder(ContentMappingBuilder()).add(IUser,ClassImplementation()).build() - hooks = collection.get_hooks(IContentMapping[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]]) - otherhooks = collection.get_hooks(IContentMapping[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_detects_interface_from_concrete_subclass(self): - collection = PyContentMappingBuilder(ContentMappingBuilder()).add(IUser,SubClassImplementation()).build() - hooks = collection.get_hooks(IContentMapping[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]]) - otherhooks = collection.get_hooks(IContentMapping[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_factory_class(self): - collection = PyContentMappingBuilder(ContentMappingBuilder()).add(ClassImplementation,IUser).build() - hooks = collection.get_hooks(IContentMapping[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]]) - otherhooks = collection.get_hooks(IContentMapping[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_factory_subclass_withinitializer(self): - def subclassinitialize(provider: IServiceProvider): - return get_service(provider, SubClassImplementation) - - collection = PyContentMappingBuilder(ContentMappingBuilder()).add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize)).build() - hooks = collection.get_hooks(IContentMapping[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]]) - otherhooks = collection.get_hooks(IContentMapping[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_factory_multipleclasses(self): - collection = PyContentMappingBuilder(ContentMappingBuilder()).add(ClassImplementation,IUser).add(SubClassImplementation,IUser).build() - hooks = collection.get_hooks(IContentMapping[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IUser]]) - otherhooks = collection.get_hooks(IContentMapping[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 2 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_callback(self): - def classcallback(context: ContentMappingContext[IProject]): - return context - - collection = PyContentMappingBuilder(ContentMappingBuilder()).add(IProject, Func[ContentMappingContext[IProject], ContentMappingContext[IProject]](classcallback)).build() - hooks = collection.get_hooks(IContentMapping[IProject]) - internalhooks = collection.get_hooks(IMigrationHook[ContentMappingContext[IProject]]) - otherhooks = collection.get_hooks(IContentMapping[IUser]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 diff --git a/src/Python/tests/test_migrations_engine_hooks_mappings_lifetime.py b/src/Python/tests/test_migrations_engine_hooks_mappings_lifetime.py deleted file mode 100644 index a09b5e1..0000000 --- a/src/Python/tests/test_migrations_engine_hooks_mappings_lifetime.py +++ /dev/null @@ -1,371 +0,0 @@ -# Copyright (c) 2024, Salesforce, Inc. -# SPDX-License-Identifier: Apache-2 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Make sure the test can find the module -import pytest -from tableau_migration.migration import get_service -from tableau_migration.migration_engine_hooks_mappings import PyContentMappingBuilder -from System import IServiceProvider, Func -from Microsoft.Extensions.DependencyInjection import ( - ServiceProviderServiceExtensions, - ServiceCollectionServiceExtensions, - ServiceCollection, - ServiceCollectionContainerBuilderExtensions) -from Tableau.Migration.Content import IUser,IProject -from Tableau.Migration.Engine.Hooks import IMigrationHook -from Tableau.Migration.Engine.Hooks.Mappings import ContentMappingBuilder, IContentMapping, ContentMappingContext -from Tableau.Migration.Interop.Hooks.Mappings import ISyncContentMapping - -class MappingImplementation(ISyncContentMapping[IUser]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings.Lifetime" - - def Execute(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]: - return ctx - -class SubMappingImplementation(MappingImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks.Mappings.Lifetime" - - def Execute(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]: - return ctx - -class TestContentMappingBuilderLifetimeTests(): - @pytest.fixture(autouse=True, scope="class") - def setup(self): - TestContentMappingBuilderLifetimeTests.MappingImplementation = MappingImplementation - TestContentMappingBuilderLifetimeTests.SubMappingImplementation = SubMappingImplementation - - yield - - del TestContentMappingBuilderLifetimeTests.SubMappingImplementation - del TestContentMappingBuilderLifetimeTests.MappingImplementation - - def test_lifetime_buildandcreate_object(self, skip_by_python_lifetime_env_var): - try: - # Arrange - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hook = TestContentMappingBuilderLifetimeTests.SubMappingImplementation() - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(IUser,hook).build().get_hooks(IContentMapping[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert hook == firstScopeHook1 - assert hook is not firstScopeHook1 - assert hook == firstScopeHook2 - assert hook is not firstScopeHook2 - assert hook == lastScopeHook - assert hook is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializersingleton(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddSingleton[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation,IUser).build().get_hooks(IContentMapping[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializerscoped(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddScoped[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation,IUser).build().get_hooks(IContentMapping[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializertransient(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddTransient[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation,IUser).build().get_hooks(IContentMapping[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializerobjectreference(self, skip_by_python_lifetime_env_var): - try: - # Arrange - hook = TestContentMappingBuilderLifetimeTests.SubMappingImplementation() - def subsubclassinitialize(provider: IServiceProvider): - return hook - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.SubMappingImplementation](subsubclassinitialize)).build().get_hooks(IContentMapping[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializernewobject(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return TestContentMappingBuilderLifetimeTests.SubMappingImplementation() - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.SubMappingImplementation](subsubclassinitialize)).build().get_hooks(IContentMapping[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializersingleton(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return get_service(provider,TestContentMappingBuilderLifetimeTests.SubMappingImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddSingleton[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.SubMappingImplementation](subsubclassinitialize)).build().get_hooks(IContentMapping[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializerscoped(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def classinitialize(provider: IServiceProvider): - return get_service(provider,TestContentMappingBuilderLifetimeTests.MappingImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddScoped[TestContentMappingBuilderLifetimeTests.MappingImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.MappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.MappingImplementation](classinitialize)).build().get_hooks(IContentMapping[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializertransient(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subclassinitialize(provider: IServiceProvider): - return get_service(provider,TestContentMappingBuilderLifetimeTests.SubMappingImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddTransient[TestContentMappingBuilderLifetimeTests.SubMappingImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(TestContentMappingBuilderLifetimeTests.SubMappingImplementation, IUser, Func[IServiceProvider, TestContentMappingBuilderLifetimeTests.SubMappingImplementation](subclassinitialize)).build().get_hooks(IContentMapping[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentMapping[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentMapping[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_callback(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def classcallback(context: ContentMappingContext[IProject]): - return context - - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentMappingBuilder(ContentMappingBuilder()).add(IProject, Func[ContentMappingContext[IProject], ContentMappingContext[IProject]](classcallback)).build().get_hooks(IContentMapping[IProject]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[ContentMappingContext[IProject]]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[ContentMappingContext[IProject]]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[ContentMappingContext[IProject]]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() diff --git a/src/Python/tests/test_migrations_engine_hooks_transformers.py b/src/Python/tests/test_migrations_engine_hooks_transformers.py deleted file mode 100644 index e908235..0000000 --- a/src/Python/tests/test_migrations_engine_hooks_transformers.py +++ /dev/null @@ -1,258 +0,0 @@ -# Copyright (c) 2024, Salesforce, Inc. -# SPDX-License-Identifier: Apache-2 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Make sure the test can find the module -from tableau_migration.migration import get_service -from tableau_migration.migration_engine_hooks_transformers import PyContentTransformerBuilder -from System import IServiceProvider, Func, ArgumentException, NotImplementedException -from System.Threading import CancellationToken -from Microsoft.Extensions.DependencyInjection import ( - ServiceCollection, - ServiceCollectionContainerBuilderExtensions) -from Tableau.Migration.Content import IUser, IProject, IWorkbook -from Tableau.Migration.Engine.Hooks import IMigrationHook -from Tableau.Migration.Engine.Hooks.Transformers import ContentTransformerBuilder, IContentTransformer, ContentTransformerBase -from Tableau.Migration.Interop.Hooks.Transformers import ISyncContentTransformer, ISyncXmlContentTransformer -from Tableau.Migration.Tests import TestFileContentType as PyTestFileContentType # Needed as this class name starts with Test, which means pytest wants to pick it up - -class ClassImplementation(ISyncContentTransformer[IUser]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers" - - def Execute(self, ctx: IUser) -> IUser: - return ctx - -class SubClassImplementation(ClassImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers" - - def Execute(self, ctx: IUser) -> IUser: - return ctx - -class RawHook(IMigrationHook[float]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers" - -class WithoutImplementation(ISyncContentTransformer[str]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers" - -class WithImplementation(ISyncContentTransformer[str]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers" - - def Execute(self, ctx: str) -> str: - return ctx - -class TestXmlTransformer(ISyncXmlContentTransformer[PyTestFileContentType]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers" - __test__ = False # Needed as this class name starts with Test, which means pytest wants to pick it up - - def __init__(self): - self.called = False - - def NeedsXmlTransforming(self, ctx: PyTestFileContentType) -> bool: - return True - - def Execute(self, ctx: PyTestFileContentType, xml) -> None: - self.called = True - -class TestContentTransformerBuilderTests(): - def test_clear_class_hook(self): - hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) - - result = hook_builder.add(IUser,ClassImplementation()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncContentTransformer[IUser])) == 0 - - def test_clear_subclass_hook(self): - hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) - - result = hook_builder.add(IUser,SubClassImplementation()).clear() - - assert result is hook_builder - assert len(result.build().get_hooks(ISyncContentTransformer[IUser])) == 0 - - def test_add_object(self): - hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) - - result = hook_builder.add(IUser,SubClassImplementation()) - - assert result is hook_builder - - def test_add_type_noinitializer(self): - hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) - - result = hook_builder.add(ClassImplementation,IUser) - - assert result is hook_builder - - def test_add_type_initializer(self): - hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) - - def subclassinitialize(provider: IServiceProvider): - return get_service(provider, SubClassImplementation) - - result = hook_builder.add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize)) - - assert result is hook_builder - - def test_add_callback(self): - hook_builder = PyContentTransformerBuilder(ContentTransformerBuilder()) - - def classcallback(context: IProject): - return context - - result = hook_builder.add(IProject, Func[IProject, IProject](classcallback)) - - assert result is hook_builder - - def test_add_withoutimplementation(self): - value = "testvalue"; - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(str,WithoutImplementation()).build().get_hooks(IContentTransformer[str]) - assert len(hookFactory) == 1 - hook = hookFactory[0].Create[IMigrationHook[str]](provider) - try: - hook.ExecuteAsync(value, CancellationToken(False)) - except NotImplementedException as nie: - assert nie.args[0] == "Python object does not have a 'Execute' method" - else: - assert False, "This test must generate a NotImplementedException." - finally: - provider.Dispose() - - def test_add_withimplementation(self): - value = "testvalue"; - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(str,WithImplementation()).build().get_hooks(IContentTransformer[str]) - assert len(hookFactory) == 1 - hook = hookFactory[0].Create[IMigrationHook[str]](provider) - try: - result = hook.ExecuteAsync(value, CancellationToken(False)) - except: - assert False, "This test must not generate an Exception." - finally: - provider.Dispose() - - def test_build_detects_interface_from_concrete_class(self): - collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IUser,ClassImplementation()).build() - hooks = collection.get_hooks(IContentTransformer[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IUser]) - otherhooks = collection.get_hooks(IContentTransformer[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_detects_interface_from_concrete_subclass(self): - collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IUser,SubClassImplementation()).build() - hooks = collection.get_hooks(IContentTransformer[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IUser]) - otherhooks = collection.get_hooks(IContentTransformer[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_factory_class(self): - collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(ClassImplementation,IUser).build() - hooks = collection.get_hooks(IContentTransformer[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IUser]) - otherhooks = collection.get_hooks(IContentTransformer[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_factory_subclass_withinitializer(self): - def subclassinitialize(provider: IServiceProvider): - return get_service(provider, SubClassImplementation) - - collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(SubClassImplementation, IUser, Func[IServiceProvider, SubClassImplementation](subclassinitialize)).build() - hooks = collection.get_hooks(IContentTransformer[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IUser]) - otherhooks = collection.get_hooks(IContentTransformer[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_factory_multipleclasses(self): - collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(ClassImplementation,IUser).add(SubClassImplementation,IUser).build() - hooks = collection.get_hooks(IContentTransformer[IUser]) - internalhooks = collection.get_hooks(IMigrationHook[IUser]) - otherhooks = collection.get_hooks(IContentTransformer[IProject]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 2 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_build_callback(self): - def classcallback(context: IProject): - return context - - collection = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IProject, Func[IProject, IProject](classcallback)).build() - hooks = collection.get_hooks(IContentTransformer[IProject]) - internalhooks = collection.get_hooks(IMigrationHook[IProject]) - otherhooks = collection.get_hooks(IContentTransformer[IUser]) - classhooks = collection.get_hooks(ClassImplementation) - subclasshooks = collection.get_hooks(SubClassImplementation) - - # Can't do a direct object comparison, so let's just count the right number is returned - assert len(hooks) == 1 - assert len(internalhooks) == 0 - assert len(otherhooks) == 0 - assert len(classhooks) == 0 - assert len(subclasshooks) == 0 - - def test_xml_execute(self): - - content = PyTestFileContentType() - - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - pyTransformer = TestXmlTransformer() - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(PyTestFileContentType, pyTransformer).build().get_hooks(IContentTransformer[PyTestFileContentType]) - assert len(hookFactory) == 1 - hook = hookFactory[0].Create[IMigrationHook[PyTestFileContentType]](provider) - try: - result = hook.ExecuteAsync(content, CancellationToken(False)).GetAwaiter().GetResult() - except Exception: - assert False, "This test must not generate an Exception." - finally: - provider.Dispose() - - assert pyTransformer.called diff --git a/src/Python/tests/test_migrations_engine_hooks_transformers_lifetime.py b/src/Python/tests/test_migrations_engine_hooks_transformers_lifetime.py deleted file mode 100644 index 002b870..0000000 --- a/src/Python/tests/test_migrations_engine_hooks_transformers_lifetime.py +++ /dev/null @@ -1,372 +0,0 @@ -# Copyright (c) 2024, Salesforce, Inc. -# SPDX-License-Identifier: Apache-2 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Make sure the test can find the module -import pytest -from tableau_migration.migration import get_service -from tableau_migration.migration_engine_hooks_transformers import PyContentTransformerBuilder -from System import IServiceProvider, Func -from Microsoft.Extensions.DependencyInjection import ( - ServiceProviderServiceExtensions, - ServiceCollectionServiceExtensions, - ServiceCollection, - ServiceCollectionContainerBuilderExtensions) -from Tableau.Migration.Content import IUser, IProject -from Tableau.Migration.Engine.Hooks import IMigrationHook -from Tableau.Migration.Engine.Hooks.Transformers import ContentTransformerBuilder, IContentTransformer -from Tableau.Migration.Interop.Hooks.Transformers import ISyncContentTransformer - -class TransformerImplementation(ISyncContentTransformer[IUser]): - __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers.Lifetime" - - def Execute(self, ctx: IUser) -> IUser: - return ctx - -class SubTransformerImplementation(TransformerImplementation): - __namespace__ = "Tableau.Migration.Custom.Hooks.Transformers.Lifetime" - - def Execute(self, ctx: IUser) -> IUser: - return ctx - -class TestContentTransformerBuilderLifetimeTests(): - @pytest.fixture(autouse=True, scope="class") - def setup(self): - TestContentTransformerBuilderLifetimeTests.TransformerImplementation = TransformerImplementation - TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation = SubTransformerImplementation - - yield - - del TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation - del TestContentTransformerBuilderLifetimeTests.TransformerImplementation - - - def test_lifetime_buildandcreate_object(self, skip_by_python_lifetime_env_var): - try: - # Arrange - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hook = TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation() - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IUser,hook).build().get_hooks(IContentTransformer[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert hook == firstScopeHook1 - assert hook is not firstScopeHook1 - assert hook == firstScopeHook2 - assert hook is not firstScopeHook2 - assert hook == lastScopeHook - assert hook is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializersingleton(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddSingleton[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation,IUser).build().get_hooks(IContentTransformer[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializerscoped(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddScoped[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation,IUser).build().get_hooks(IContentTransformer[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_noinitializertransient(self, skip_by_python_lifetime_env_var): - try: - # Arrange - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddTransient[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation,IUser).build().get_hooks(IContentTransformer[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializerobjectreference(self, skip_by_python_lifetime_env_var): - try: - # Arrange - hook = TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation() - def subsubclassinitialize(provider: IServiceProvider): - return hook - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](subsubclassinitialize)).build().get_hooks(IContentTransformer[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializernewobject(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation() - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](subsubclassinitialize)).build().get_hooks(IContentTransformer[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializersingleton(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subsubclassinitialize(provider: IServiceProvider): - return get_service(provider,TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddSingleton[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](subsubclassinitialize)).build().get_hooks(IContentTransformer[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 == lastScopeHook - assert firstScopeHook1 is not lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializerscoped(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def classinitialize(provider: IServiceProvider): - return get_service(provider,TestContentTransformerBuilderLifetimeTests.TransformerImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddScoped[TestContentTransformerBuilderLifetimeTests.TransformerImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.TransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.TransformerImplementation](classinitialize)).build().get_hooks(IContentTransformer[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 == firstScopeHook2 - assert firstScopeHook1 is not firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_initializertransient(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def subclassinitialize(provider: IServiceProvider): - return get_service(provider,TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation) - servicecollection = ServiceCollection() - ServiceCollectionServiceExtensions.AddTransient[TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](servicecollection) - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(servicecollection) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation, IUser, Func[IServiceProvider, TestContentTransformerBuilderLifetimeTests.SubTransformerImplementation](subclassinitialize)).build().get_hooks(IContentTransformer[IUser]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IContentTransformer[IUser]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IContentTransformer[IUser]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() - - def test_lifetime_buildandcreate_callback(self, skip_by_python_lifetime_env_var): - try: - # Arrange - def classcallback(context: IProject): - return context - - provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(ServiceCollection()) - hookFactory = PyContentTransformerBuilder(ContentTransformerBuilder()).add(IProject, Func[IProject, IProject](classcallback)).build().get_hooks(IContentTransformer[IProject]) - - assert len(hookFactory) == 1 - - # Act - try: - scope1 = ServiceProviderServiceExtensions.CreateScope(provider) - firstScopeHook1 = hookFactory[0].Create[IMigrationHook[IProject]](scope1.ServiceProvider) - firstScopeHook2 = hookFactory[0].Create[IMigrationHook[IProject]](scope1.ServiceProvider) - finally: - scope1.Dispose() - - try: - scope2 = ServiceProviderServiceExtensions.CreateScope(provider) - lastScopeHook = hookFactory[0].Create[IMigrationHook[IProject]](scope2.ServiceProvider) - finally: - scope2.Dispose() - - # Assert - assert firstScopeHook1 != firstScopeHook2 - assert firstScopeHook1 != lastScopeHook - finally: - provider.Dispose() diff --git a/src/Python/tests/test_other.py b/src/Python/tests/test_other.py index c5a9b3a..4632898 100644 --- a/src/Python/tests/test_other.py +++ b/src/Python/tests/test_other.py @@ -15,7 +15,11 @@ import os import logging -import tableau_migration +from tableau_migration import _logger_names +from tableau_migration.migration import ( + get_service_provider, + get_service +) from tableau_migration.migration_engine import ( PyMigrationPlanBuilder) @@ -46,9 +50,9 @@ def test_logging(self): PyMigrationPlanBuilder() # tableau_migration module keeps track of instaniated loggers. Verify that we have at least one - assert len(tableau_migration._logger_names) > 0 + assert len(_logger_names) > 0 - for name in tableau_migration._logger_names: + for name in _logger_names: # Given that we have a name, we should have a logger assert logging.getLogger(name) @@ -62,8 +66,8 @@ def test_config(self): process starts and hence the env variables take and that way an int can be set to a env var. ''' - services = tableau_migration.migration.get_service_provider() - config_reader = tableau_migration.migration.get_service(services, IConfigReader) + services = get_service_provider() + config_reader = get_service(services, IConfigReader) batch_size = config_reader.Get[IUser]().BatchSize diff --git a/src/Tableau.Migration.PythonGenerator/Config/Hints/GeneratorHints.cs b/src/Tableau.Migration.PythonGenerator/Config/Hints/GeneratorHints.cs new file mode 100644 index 0000000..b71af0e --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Config/Hints/GeneratorHints.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Config.Hints +{ + internal sealed class GeneratorHints + { + public NamespaceHints[] Namespaces { get; set; } = Array.Empty(); + + public TypeHints? ForType(ITypeSymbol type) + { + var ns = type.ContainingNamespace.ToDisplayString(); + var nsHint = Namespaces.FirstOrDefault(nh => string.Equals(nh.Namespace, ns, StringComparison.Ordinal)); + if(nsHint is null) + { + return null; + } + + return nsHint.Types.FirstOrDefault(th => string.Equals(th.Type, type.Name, StringComparison.Ordinal)); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Config/Hints/NamespaceHints.cs b/src/Tableau.Migration.PythonGenerator/Config/Hints/NamespaceHints.cs new file mode 100644 index 0000000..12632e7 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Config/Hints/NamespaceHints.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Tableau.Migration.PythonGenerator.Config.Hints +{ + internal sealed class NamespaceHints + { + public string Namespace { get; set; } = string.Empty; + + public TypeHints[] Types { get; set; } = Array.Empty(); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Config/Hints/TypeHints.cs b/src/Tableau.Migration.PythonGenerator/Config/Hints/TypeHints.cs new file mode 100644 index 0000000..b47e276 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Config/Hints/TypeHints.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Tableau.Migration.PythonGenerator.Config.Hints +{ + internal sealed class TypeHints + { + public string Type { get; set; } = string.Empty; + + public string[] ExcludeMembers { get; set; } = Array.Empty(); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Config/PythonGeneratorOptions.cs b/src/Tableau.Migration.PythonGenerator/Config/PythonGeneratorOptions.cs new file mode 100644 index 0000000..ce97794 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Config/PythonGeneratorOptions.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Tableau.Migration.PythonGenerator.Config.Hints; + +namespace Tableau.Migration.PythonGenerator.Config +{ + internal sealed class PythonGeneratorOptions + { + public string ImportPath { get; set; } = string.Empty; + + public string OutputPath { get; set; } = string.Empty; + + public GeneratorHints Hints { get; set; } = new(); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/ConversionMode.cs b/src/Tableau.Migration.PythonGenerator/ConversionMode.cs new file mode 100644 index 0000000..e069ad4 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/ConversionMode.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator +{ + internal enum ConversionMode + { + Direct = 0, + + Wrap, + + WrapSerialized, + + WrapGeneric, + + Enum, + + WrapImmutableCollection, + + WrapMutableCollection, + + WrapArray + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonDocstringGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonDocstringGenerator.cs new file mode 100644 index 0000000..efbfb61 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonDocstringGenerator.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal interface IPythonDocstringGenerator + { + PythonDocstring? Generate(ISymbol dotNetSymbol, bool ignoreArgs = false); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonEnumValueGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonEnumValueGenerator.cs new file mode 100644 index 0000000..2949148 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonEnumValueGenerator.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal interface IPythonEnumValueGenerator + { + ImmutableArray GenerateEnumValues(INamedTypeSymbol dotNetType); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonGenerator.cs new file mode 100644 index 0000000..84a864f --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonGenerator.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal interface IPythonGenerator + { + PythonTypeCache Generate(IEnumerable dotNetTypes); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonMethodGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonMethodGenerator.cs new file mode 100644 index 0000000..9000d52 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonMethodGenerator.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal interface IPythonMethodGenerator + { + ImmutableArray GenerateMethods(INamedTypeSymbol dotNetType); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonPropertyGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonPropertyGenerator.cs new file mode 100644 index 0000000..76cb66d --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonPropertyGenerator.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal interface IPythonPropertyGenerator + { + ImmutableArray GenerateProperties(INamedTypeSymbol dotNetType); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/IPythonTypeGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/IPythonTypeGenerator.cs new file mode 100644 index 0000000..f8ef6c1 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/IPythonTypeGenerator.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal interface IPythonTypeGenerator + { + PythonType Generate(ImmutableHashSet dotNetTypeNames, INamedTypeSymbol dotNetType); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonDocstringGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonDocstringGenerator.cs new file mode 100644 index 0000000..1a6ef05 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonDocstringGenerator.cs @@ -0,0 +1,111 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using System.Xml; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal sealed class PythonDocstringGenerator : IPythonDocstringGenerator + { + [return: NotNullIfNotNull(nameof(s))] + private static string? CleanText(string? s) + { + if (s is null) + { + return null; + } + + var result = Regex.Replace(s.Trim().ReplaceLineEndings(""), @"\s+", " "); + + if (result.Contains("\w+)(?:`\d)*\""\s+/\>", "$1"); + result = Regex.Replace(result, @"\.*)\""\s*\>(?.+)\", "$1"); + } + + if (result.Contains("\w+)\""\s+/\>", "$1"); + } + + if (result.Contains("para>")) + { + result = Regex.Replace(result, @"\<(/*)para\>", string.Empty); + } + + return result.Trim(); + } + + private static PythonDocstring FromXml(string xmlDoc, bool ignoreArgs) + { + var xml = new XmlDocument(); + xml.LoadXml(xmlDoc); + + string summary = string.Empty; + var summaryEls = xml.DocumentElement?.GetElementsByTagName("summary"); + if (summaryEls is not null && summaryEls.Count > 0) + { + summary = CleanText(summaryEls[0]?.InnerXml) ?? string.Empty; + } + + string? returnDoc = null; + var returnsEls = xml.DocumentElement?.GetElementsByTagName("returns"); + if (returnsEls is not null && returnsEls.Count > 0) + { + returnDoc = CleanText(returnsEls[0]?.InnerXml); + } + + var args = ImmutableArray.CreateBuilder(); + + if(!ignoreArgs) + { + var argEls = xml.DocumentElement?.GetElementsByTagName("param"); + if (argEls is not null && argEls.Count > 0) + { + for (int i = 0; i < argEls.Count; i++) + { + var arg = argEls[i]; + var name = arg?.Attributes?["name"]?.Value?.ToSnakeCase(); + if (arg is null || name is null) + { + continue; + } + + args.Add(new(name, CleanText(arg.InnerXml))); + } + } + } + + return new(summary, args.ToImmutable(), returnDoc); + } + + public PythonDocstring? Generate(ISymbol dotNetSymbol, bool ignoreArgs = false) + { + var xml = dotNetSymbol.GetDocumentationCommentXml(); + if (string.IsNullOrEmpty(xml)) + { + return null; + } + + return FromXml(xml, ignoreArgs); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonEnumValueGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonEnumValueGenerator.cs new file mode 100644 index 0000000..916a4e7 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonEnumValueGenerator.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; +using Tableau.Migration.PythonGenerator.Config; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal sealed class PythonEnumValueGenerator : PythonMemberGenerator, IPythonEnumValueGenerator + { + private readonly IPythonDocstringGenerator _docGenerator; + + public PythonEnumValueGenerator(IPythonDocstringGenerator docGenerator, + IOptions options) + : base(options) + { + _docGenerator = docGenerator; + } + + public ImmutableArray GenerateEnumValues(INamedTypeSymbol dotNetType) + { + if(!dotNetType.IsAnyEnum()) + { + return ImmutableArray.Empty; + } + + var results = ImmutableArray.CreateBuilder(); + + foreach (var dotNetMember in dotNetType.GetMembers()) + { + if (!(dotNetMember is IFieldSymbol dotNetField) || + !dotNetField.IsConst || + IgnoreMember(dotNetType, dotNetField)) + { + continue; + } + + var value = dotNetField.ConstantValue!; + var docs = _docGenerator.Generate(dotNetField); + + var pyValue = new PythonEnumValue(dotNetField.Name.ToConstantCase(), value, docs); + + results.Add(pyValue); + } + + return results.ToImmutable(); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonGenerator.cs new file mode 100644 index 0000000..687aaa6 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonGenerator.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal sealed class PythonGenerator : IPythonGenerator + { + private readonly IPythonTypeGenerator _typeGenerator; + + public PythonGenerator(IPythonTypeGenerator typeGenerator) + { + _typeGenerator = typeGenerator; + } + + public PythonTypeCache Generate(IEnumerable dotNetTypes) + { + var dotNetTypeNames = dotNetTypes.Select(t => t.ToDisplayString()).ToImmutableHashSet(); + + var pyTypes = new List(dotNetTypes.Count()); + + foreach(var dotnetType in dotNetTypes) + { + var pyType = _typeGenerator.Generate(dotNetTypeNames,dotnetType); + pyTypes.Add(pyType); + } + + return new PythonTypeCache(pyTypes); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonMemberGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonMemberGenerator.cs new file mode 100644 index 0000000..1eef6e5 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonMemberGenerator.cs @@ -0,0 +1,195 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; +using Tableau.Migration.PythonGenerator.Config; +using Dotnet = Tableau.Migration.PythonGenerator.Keywords.Dotnet; +using Py = Tableau.Migration.PythonGenerator.Keywords.Python; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal abstract class PythonMemberGenerator + { + private static readonly PythonTypeReference BOOL = new(Py.Types.BOOL, ImportModule: null, ConversionMode.Direct); + private static readonly PythonTypeReference INT = new(Py.Types.INT, ImportModule: null, ConversionMode.Direct); + private static readonly PythonTypeReference STRING = new(Py.Types.STR, ImportModule: null, ConversionMode.Direct); + private static readonly PythonTypeReference UUID = new( + Py.Types.UUID, + ImportModule: Py.Modules.UUID, + ConversionMode.WrapSerialized, + DotNetParseFunction: "Guid.Parse", + ExtraImports: ImmutableArray.Create( + new PythonTypeReference(Dotnet.Types.GUID, ImportModule: Dotnet.Namespaces.SYSTEM, ConversionMode.Direct))); + + internal static readonly PythonTypeReference LIST_REFERENCE = new PythonTypeReference( + Dotnet.Types.LIST, + ImportModule: Dotnet.Namespaces.SYSTEM_COLLECTIONS_GENERIC, + ConversionMode: ConversionMode.Direct, + ImportAlias: Dotnet.TypeAliases.LIST); + + internal static readonly PythonTypeReference HASH_SET_REFERENCE = new PythonTypeReference( + Dotnet.Types.HASH_SET, + ImportModule: Dotnet.Namespaces.SYSTEM_COLLECTIONS_GENERIC, + ConversionMode: ConversionMode.Direct, + ImportAlias: Dotnet.TypeAliases.HASH_SET); + + private static readonly PythonTypeReference STRING_REFERENCE = new PythonTypeReference( + Dotnet.Types.STRING, + ImportModule: Dotnet.Namespaces.SYSTEM, + ConversionMode: ConversionMode.Direct, + ImportAlias: Dotnet.TypeAliases.STRING); + + private static readonly PythonTypeReference EXCEPTION = new( + Dotnet.Namespaces.SYSTEM_EXCEPTION, + Dotnet.Namespaces.SYSTEM, + ConversionMode.Direct); + + private readonly PythonGeneratorOptions _options; + + protected PythonMemberGenerator(IOptions options) + { + _options = options.Value; + } + + protected bool IgnoreMember(ITypeSymbol type, ISymbol member) + { + var typeHints = _options.Hints.ForType(type); + if (typeHints is null) + { + return false; + } + + return typeHints.ExcludeMembers.Any(m => string.Equals(m, member.Name, StringComparison.Ordinal)); + } + + protected ImmutableArray? GetGenericTypes(ITypeSymbol t) + { + if (t is INamedTypeSymbol nt) + { + return nt.TypeArguments.Select(ToPythonType).ToImmutableArray(); + } + + if (t is IArrayTypeSymbol at) + { + var pyType = ToPythonType(at.ElementType); + return ImmutableArray.Create(pyType); + } + + return null; + } + + protected ImmutableArray? GetDotnetGenericTypes(ITypeSymbol t) + { + if (t is INamedTypeSymbol nt) + { + return nt.TypeArguments; + } + + if (t is IArrayTypeSymbol at) + { + return ImmutableArray.Create(at.ElementType); + } + return null; + } + + protected PythonTypeReference ToPythonType(ITypeSymbol t) + { + if (t.Kind is SymbolKind.TypeParameter) + { + return PythonTypeReference.ForGenericType(t); + } + + switch (t.Name) + { + case "bool": + case nameof(Boolean): + return BOOL; + case nameof(Exception): + return EXCEPTION; + case nameof(Guid): + return UUID; + case nameof(IList): + return new( + Py.Types.LIST_WRAPPED, + ImportModule: Py.Modules.TYPING, + ConversionMode.WrapMutableCollection, + GenericTypes: GetGenericTypes(t), + ExtraImports: ImmutableArray.Create(LIST_REFERENCE), + DotnetTypes: GetDotnetGenericTypes(t)); + case nameof(HashSet): + return new( + Py.Types.SET, + ImportModule: Py.Modules.TYPING, + ConversionMode.WrapMutableCollection, + GenericTypes: GetGenericTypes(t), + ExtraImports: ImmutableArray.Create(HASH_SET_REFERENCE), + DotnetTypes: GetDotnetGenericTypes(t)); + case nameof(ISet): + return new( + Py.Types.SEQUENCE, + ImportModule: Py.Modules.TYPING, + ConversionMode.WrapMutableCollection, + GenericTypes: GetGenericTypes(t), + ExtraImports: ImmutableArray.Create(LIST_REFERENCE, STRING_REFERENCE, HASH_SET_REFERENCE), + DotnetTypes: GetDotnetGenericTypes(t)); + + case nameof(ImmutableArray): + case nameof(IImmutableList): + case nameof(IReadOnlyList): + case nameof(IEnumerable): + return new( + Py.Types.SEQUENCE, + ImportModule: Py.Modules.TYPING, + ConversionMode.WrapImmutableCollection, + WrapType: "list", + GenericTypes: GetGenericTypes(t)); + case Dotnet.Types.INT: + case nameof(Int32): + case Dotnet.Types.LONG: + case nameof(Int64): + return INT; + case nameof(Nullable): + return GetGenericTypes(t)!.Value.Single(); + case Dotnet.Types.STRING_SIMPLIFIED: + case nameof(String): + return STRING; + default: + if (t is IArrayTypeSymbol symbol) + { + return new( + Py.Types.SEQUENCE, + ImportModule: Py.Modules.TYPING, + ConversionMode.WrapArray, + WrapType: "list", + GenericTypes: GetGenericTypes(t), + ExtraImports: ImmutableArray.Create(LIST_REFERENCE), + DotnetTypes: GetDotnetGenericTypes(t)); + } + else + { + return PythonTypeReference.ForDotNetType(t); + } + } + } + + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonMethodGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonMethodGenerator.cs new file mode 100644 index 0000000..014c9ee --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonMethodGenerator.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; +using Tableau.Migration.PythonGenerator.Config; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal class PythonMethodGenerator : PythonMemberGenerator, IPythonMethodGenerator + { + private static readonly ImmutableHashSet IGNORED_METHODS = new[] + { + nameof(object.Equals), + nameof(object.GetHashCode), + nameof(object.ToString), + nameof(IComparable.CompareTo), + "Deconstruct", + "PrintMembers", + "$" + }.ToImmutableHashSet(); + + private readonly IPythonDocstringGenerator _docGenerator; + + public PythonMethodGenerator(IPythonDocstringGenerator docGenerator, + IOptions options) + : base(options) + { + _docGenerator = docGenerator; + } + + private PythonTypeReference? GetPythonReturnType(IMethodSymbol dotNetMethod) + { + if(dotNetMethod.ReturnsVoid) + { + return null; + } + + return ToPythonType(dotNetMethod.ReturnType); + } + + private ImmutableArray GenerateArguments(IMethodSymbol dotNetMethod) + { + var results = ImmutableArray.CreateBuilder(); + + foreach(var dotNetParam in dotNetMethod.Parameters) + { + var pyArgument = new PythonMethodArgument(dotNetParam.Name.ToSnakeCase(), ToPythonType(dotNetParam.Type)); + results.Add(pyArgument); + } + + return results.ToImmutable(); + } + + public ImmutableArray GenerateMethods(INamedTypeSymbol dotNetType) + { + var results = ImmutableArray.CreateBuilder(); + + foreach (var dotNetMember in dotNetType.GetMembers()) + { + if (!(dotNetMember is IMethodSymbol dotNetMethod) || IgnoreMember(dotNetType, dotNetMethod)) + { + continue; + } + + if(dotNetMethod.MethodKind is not MethodKind.Ordinary || IGNORED_METHODS.Contains(dotNetMethod.Name)) + { + continue; + } + + var docs = _docGenerator.Generate(dotNetMethod); + var returnType = GetPythonReturnType(dotNetMethod); + var arguments = GenerateArguments(dotNetMethod); + + var pyMethod = new PythonMethod(dotNetMethod.Name.ToSnakeCase(), returnType, arguments, dotNetMethod.IsStatic, docs, dotNetMethod); + + results.Add(pyMethod); + } + + return results.ToImmutable(); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonPropertyGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonPropertyGenerator.cs new file mode 100644 index 0000000..4ba1133 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonPropertyGenerator.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; +using Tableau.Migration.PythonGenerator.Config; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal sealed class PythonPropertyGenerator : PythonMemberGenerator, IPythonPropertyGenerator + { + private static readonly ImmutableHashSet IGNORED_PROPERTIES = new[] + { + "EqualityContract" + }.ToImmutableHashSet(); + + private readonly IPythonDocstringGenerator _docGenerator; + + public PythonPropertyGenerator(IPythonDocstringGenerator docGenerator, + IOptions options) + : base(options) + { + _docGenerator = docGenerator; + } + + public ImmutableArray GenerateProperties(INamedTypeSymbol dotNetType) + { + var results = ImmutableArray.CreateBuilder(); + + foreach (var dotNetMember in dotNetType.GetMembers()) + { + if(dotNetMember.IsStatic || + !(dotNetMember is IPropertySymbol dotNetProperty) || + IgnoreMember(dotNetType, dotNetProperty) || + IGNORED_PROPERTIES.Contains(dotNetProperty.Name)) + { + continue; + } + + var type = ToPythonType(dotNetProperty.Type); + var docs = _docGenerator.Generate(dotNetProperty); + + var pyProperty = new PythonProperty(dotNetProperty.Name.ToSnakeCase(), type, + !dotNetProperty.IsWriteOnly, + !(dotNetProperty.IsReadOnly || (dotNetProperty.SetMethod is not null && dotNetProperty.SetMethod.IsInitOnly)), + docs, dotNetProperty); + + results.Add(pyProperty); + } + + return results.ToImmutable(); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Generators/PythonTypeGenerator.cs b/src/Tableau.Migration.PythonGenerator/Generators/PythonTypeGenerator.cs new file mode 100644 index 0000000..3282bea --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Generators/PythonTypeGenerator.cs @@ -0,0 +1,137 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Generators +{ + internal sealed class PythonTypeGenerator : IPythonTypeGenerator + { + private readonly IPythonPropertyGenerator _propertyGenerator; + private readonly IPythonMethodGenerator _methodGenerator; + private readonly IPythonEnumValueGenerator _enumValueGenerator; + private readonly IPythonDocstringGenerator _docGenerator; + + public PythonTypeGenerator(IPythonPropertyGenerator propertyGenerator, + IPythonMethodGenerator methodGenerator, + IPythonEnumValueGenerator enumValueGenerator, + IPythonDocstringGenerator docGenerator) + { + _propertyGenerator = propertyGenerator; + _methodGenerator = methodGenerator; + _enumValueGenerator = enumValueGenerator; + _docGenerator = docGenerator; + } + + private bool HasInterface(INamedTypeSymbol type, INamedTypeSymbol test) + { + foreach (var childInterface in type.AllInterfaces) + { + if(string.Equals(childInterface.ToDisplayString(), test.ToDisplayString(), System.StringComparison.Ordinal)) + { + return true; + } + + if (HasInterface(childInterface, test)) + { + return true; + } + } + + return false; + } + + private (ImmutableArray InheritedTypes, ImmutableArray ExcludedInterfaces) + GenerateInheritedTypes(ImmutableHashSet dotNetTypeNames, INamedTypeSymbol dotNetType) + { + var inheritedTypes = ImmutableArray.CreateBuilder(); + + if(dotNetType.TypeArguments.Any()) + { + var genericTypes = dotNetType.TypeArguments + .Select(PythonTypeReference.ForGenericType) + .ToImmutableArray(); + + var extraImports = ImmutableArray.Create( + new PythonTypeReference("TypeVar", "typing", ConversionMode.Direct), + new PythonTypeReference("_generic_wrapper", "tableau_migration.migration", ConversionMode.Direct) + ); + + inheritedTypes.Add(new("Generic", "typing", ConversionMode.Direct, genericTypes, ExtraImports: extraImports)); + } + + if(dotNetType.IsOrdinalEnum()) + { + inheritedTypes.Add(new("IntEnum", "enum", ConversionMode.Direct)); + } + else if(dotNetType.IsStringEnum()) + { + inheritedTypes.Add(new("StrEnum", "migration_enum", ConversionMode.Direct)); + } + + var interfaces = new List(); + var excludedInterfaces = ImmutableArray.CreateBuilder(); + + foreach (var interfaceType in dotNetType.AllInterfaces) + { + if(dotNetTypeNames.Contains(interfaceType.ToDisplayString())) + { + interfaces.Add(interfaceType); + } + else + { + excludedInterfaces.Add(interfaceType); + } + } + + // Remove interfaces implemented by other inherited interfaces, + // to reduce multi-inheritance complexity. + foreach(var interfaceType in interfaces.ToImmutableArray()) + { + if(interfaces.Any(i => HasInterface(i, interfaceType))) + { + interfaces.Remove(interfaceType); + } + } + + inheritedTypes.AddRange(interfaces.Select(PythonTypeReference.ForDotNetType)); + + return (inheritedTypes.ToImmutable(), excludedInterfaces.ToImmutable()); + } + + public PythonType Generate(ImmutableHashSet dotNetTypeNames, INamedTypeSymbol dotNetType) + { + (var inheritedTypes, var excludedInterfaces) = GenerateInheritedTypes(dotNetTypeNames, dotNetType); + + var properties = _propertyGenerator.GenerateProperties(dotNetType); + var methods = _methodGenerator.GenerateMethods(dotNetType); + var enumValues = _enumValueGenerator.GenerateEnumValues(dotNetType); + + // arg docs at the type level would be for record properties. + var docs = _docGenerator.Generate(dotNetType, ignoreArgs: true); + + var typeRef = PythonTypeReference.ForDotNetType(dotNetType); + + return new(typeRef.Name, typeRef.ImportModule!, inheritedTypes, + properties, methods, enumValues, + docs, dotNetType, excludedInterfaces); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/INamedTypeSymbolExtensions.cs b/src/Tableau.Migration.PythonGenerator/INamedTypeSymbolExtensions.cs new file mode 100644 index 0000000..900bf85 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/INamedTypeSymbolExtensions.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator +{ + public static class INamedTypeSymbolExtensions + { + public static bool IsOrdinalEnum(this INamedTypeSymbol t) + => t.EnumUnderlyingType is not null || string.Equals(t.BaseType?.Name, nameof(Enum), StringComparison.Ordinal); + + public static bool IsStringEnum(this INamedTypeSymbol t) + => string.Equals(t.BaseType?.Name, nameof(StringEnum), StringComparison.Ordinal); + + public static bool IsAnyEnum(this INamedTypeSymbol t) + => t.IsOrdinalEnum() || t.IsStringEnum(); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/ITypeSymbolExtensions.cs b/src/Tableau.Migration.PythonGenerator/ITypeSymbolExtensions.cs new file mode 100644 index 0000000..546076d --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/ITypeSymbolExtensions.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator +{ + internal static class ITypeSymbolExtensions + { + public static string ToPythonModuleName(this ITypeSymbol ts) + { + var containingNs = ts.ContainingNamespace; + if (containingNs == null) + { + return ts.Name; + } + + return "tableau_migration." + containingNs.ToDisplayString() + .Replace("Tableau.", "") + .Replace(".", "_") + .ToLower(); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/IndentingStringBuilder.cs b/src/Tableau.Migration.PythonGenerator/IndentingStringBuilder.cs new file mode 100644 index 0000000..a96c8f5 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/IndentingStringBuilder.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Text; + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed class IndentingStringBuilder : IDisposable + { + private readonly string _newLine; + private readonly uint _indentLevel; + + private const string INDENT = " "; + + public StringBuilder NonIndentingBuilder { get; } + + public IndentingStringBuilder(StringBuilder builder, uint indentLevel = 0) + : this(builder, Environment.NewLine, indentLevel) + { } + + public IndentingStringBuilder(StringBuilder builder, string newLine, uint indentLevel = 0) + { + NonIndentingBuilder = builder; + _newLine = newLine; + _indentLevel = indentLevel; + } + + private void Indent() + { + for(uint i = 0; i < _indentLevel; i++) + { + NonIndentingBuilder.Append(INDENT); + } + } + + public IndentingStringBuilder AppendLine(string? text = null) + { + Indent(); + NonIndentingBuilder.Append(text); + NonIndentingBuilder.Append(_newLine); + return this; + } + + public IndentingStringBuilder AppendLineAndIndent(string text) + { + AppendLine(text); + return new IndentingStringBuilder(NonIndentingBuilder, _indentLevel + 1); + } + + public override string ToString() => NonIndentingBuilder.ToString(); + + public void Dispose() + { } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Keywords/Dotnet/Namespaces.cs b/src/Tableau.Migration.PythonGenerator/Keywords/Dotnet/Namespaces.cs new file mode 100644 index 0000000..1527331 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Keywords/Dotnet/Namespaces.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Keywords.Dotnet +{ + internal class Namespaces + { + public const string SYSTEM = "System"; + public const string TABLEAU_MIGRATION = "Tableau.Migration"; + public const string SYSTEM_COLLECTIONS_GENERIC = "System.Collections.Generic"; + public const string SYSTEM_EXCEPTION = "System.Exception"; + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Keywords/Dotnet/TypeAliases.cs b/src/Tableau.Migration.PythonGenerator/Keywords/Dotnet/TypeAliases.cs new file mode 100644 index 0000000..0ad80b1 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Keywords/Dotnet/TypeAliases.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Keywords.Dotnet +{ + internal class TypeAliases + { + public const string LIST = "DotnetList"; + public const string HASH_SET = "DotnetHashSet"; + public const string STRING = "String"; + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Keywords/Dotnet/Types.cs b/src/Tableau.Migration.PythonGenerator/Keywords/Dotnet/Types.cs new file mode 100644 index 0000000..74a6d4f --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Keywords/Dotnet/Types.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Keywords.Dotnet +{ + internal class Types + { + public const string NULLABLE = "Nullable"; + public const string LIST = "List"; + public const string BOOLEAN = "Boolean"; + public const string HASH_SET = "HashSet"; + public const string STRING = "String"; + public const string STRING_SIMPLIFIED = "string"; + public const string GUID = "Guid"; + public const string LONG = "long"; + public const string INT = "int"; + + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Keywords/Python/Modules.cs b/src/Tableau.Migration.PythonGenerator/Keywords/Python/Modules.cs new file mode 100644 index 0000000..4af5266 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Keywords/Python/Modules.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Keywords.Python +{ + internal static class Modules + { + + public const string AUTOFIXTURE = "tests.helpers.autofixture"; + public const string UUID = "uuid"; + public const string TYPING = "typing"; + } +} \ No newline at end of file diff --git a/src/Tableau.Migration.PythonGenerator/Keywords/Python/Types.cs b/src/Tableau.Migration.PythonGenerator/Keywords/Python/Types.cs new file mode 100644 index 0000000..202de7b --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Keywords/Python/Types.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Keywords.Python +{ + internal static class Types + { + public const string AUTOFIXTURE_TESTBASE = "AutoFixtureTestBase"; + public const string UUID = "UUID"; + public const string SEQUENCE = "Sequence"; + public const string SET = "Set"; + public const string LIST_WRAPPED = "List"; + public const string BOOL = "bool"; + public const string STR = "str"; + public const string INT = "int"; + } +} \ No newline at end of file diff --git a/src/Tableau.Migration.PythonGenerator/Program.cs b/src/Tableau.Migration.PythonGenerator/Program.cs new file mode 100644 index 0000000..c074996 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Program.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Tableau.Migration.PythonGenerator.Config; +using Tableau.Migration.PythonGenerator.Generators; +using Tableau.Migration.PythonGenerator.Writers; + +namespace Tableau.Migration.PythonGenerator +{ + public static class Program + { + public static async Task Main(string[] args) + { + using var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((ctx, services) => + { + services + .Configure(ctx.Configuration) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddHostedService(); + }) + .Build(); + + await host.RunAsync(); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/PythonArgDocstring.cs b/src/Tableau.Migration.PythonGenerator/PythonArgDocstring.cs new file mode 100644 index 0000000..49eb0d7 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonArgDocstring.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed record PythonArgDocstring(string Name, string Documentation) + { } +} diff --git a/src/Tableau.Migration.PythonGenerator/PythonDocstring.cs b/src/Tableau.Migration.PythonGenerator/PythonDocstring.cs new file mode 100644 index 0000000..2603786 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonDocstring.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Immutable; +using System.Linq; + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed record PythonDocstring(string Summary, ImmutableArray Args, string? Returns = null) + { + /// + /// Gets whether or not the docstring has "extra info," + /// i.e. more than just a summary. + /// + public bool HasExtraInfo => HasReturns || HasArgs; + + /// + /// Gets whether or not the docstring has a string for a return value. + /// + public bool HasReturns => !string.IsNullOrWhiteSpace(Returns); + + /// + /// Gets whether or not the docstring has a strings for a arguments. + /// + public bool HasArgs => Args.Any(); + + public PythonDocstring(string summary) + : this(summary, ImmutableArray.Empty) + { } + + public PythonDocstring(string summary, string returns, params (string ArgName, string ArgValue)[] args) + : this(summary, args.Select(a => new PythonArgDocstring(a.ArgName, a.ArgValue)).ToImmutableArray(), returns) + { } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/PythonEnumValue.cs b/src/Tableau.Migration.PythonGenerator/PythonEnumValue.cs new file mode 100644 index 0000000..5885760 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonEnumValue.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed record PythonEnumValue(string Name, object Value, PythonDocstring? Documentation) + { } +} diff --git a/src/Tableau.Migration.PythonGenerator/PythonGenerationList.cs b/src/Tableau.Migration.PythonGenerator/PythonGenerationList.cs new file mode 100644 index 0000000..5e65cdd --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonGenerationList.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Tableau.Migration.Api.Rest.Models; +using Tableau.Migration.Api.Rest.Models.Types; +using Tableau.Migration.Content; +using Tableau.Migration.Content.Permissions; +using Tableau.Migration.Engine; +using Tableau.Migration.Engine.Actions; +using Tableau.Migration.Engine.Hooks.Mappings; +using Tableau.Migration.Engine.Hooks.PostPublish; +using Tableau.Migration.Engine.Manifest; +using Tableau.Migration.Engine.Migrators; +using Tableau.Migration.Engine.Migrators.Batch; + +namespace Tableau.Migration.PythonGenerator +{ + internal static class PythonGenerationList + { + private static readonly ImmutableHashSet TYPES_TO_GENERATE = ToTypeNameHash( + typeof(ContentLocation), + typeof(IContentReference), + typeof(IResult), + typeof(MigrationCompletionStatus), + + typeof(MigrationManifestEntryStatus), + typeof(IMigrationManifestEntry), + typeof(IMigrationManifestEntryEditor), + + typeof(IConnectionsContent), + typeof(IContainerContent), + typeof(IWithDomain), + typeof(IUsernameContent), + typeof(IUser), + typeof(IGroup), + typeof(IDescriptionContent), + typeof(IProject), + typeof(IExtractContent), + typeof(IPublishedContent), + typeof(IDataSource), + typeof(IWorkbook), + typeof(IConnection), + typeof(IDataSourceDetails), + typeof(IView), + typeof(ITag), + typeof(IPublishableDataSource), + typeof(IPublishableWorkbook), + typeof(IGroupUser), + typeof(IWorkbookDetails), + typeof(ILabel), + + typeof(ContentMigrationItem<>), + typeof(ContentItemPostPublishContext<,>), + typeof(ContentMappingContext<>), + typeof(BulkPostPublishContext<>), + typeof(IContentItemMigrationResult<>), + typeof(IContentBatchMigrationResult<>), + typeof(IMigrationActionResult), + typeof(IPublishableGroup), + typeof(IWithTags), + typeof(IWithOwner), + + typeof(AdministratorLevels), + typeof(ContentPermissions), + typeof(ExtractEncryptionModes), + typeof(LabelCategories), + typeof(LicenseLevels), + typeof(PermissionsCapabilityModes), + typeof(PermissionsCapabilityNames), + typeof(SiteRoles), + typeof(AuthenticationTypes), + typeof(DataSourceFileTypes), + typeof(WorkbookFileTypes), + typeof(GranteeType), + typeof(IGranteeCapability), + typeof(IPermissions), + typeof(ICapability) + ); + + private static ImmutableHashSet ToTypeNameHash(params Type[] types) + => types.Select(t => t.FullName!).ToImmutableHashSet(); + + private static IEnumerable FindTypesToGenerateForNamespace(INamespaceSymbol ns) + { + foreach (var type in ns.GetTypeMembers()) + { + var typeName = $"{type.ContainingNamespace.ToDisplayString()}.{type.MetadataName}"; + if (TYPES_TO_GENERATE.Contains(typeName)) + { + yield return type; + } + } + + foreach (var childNamespace in ns.GetNamespaceMembers()) + { + foreach (var type in FindTypesToGenerateForNamespace(childNamespace)) + { + yield return type; + } + } + } + + internal static ImmutableArray FindTypesToGenerate(INamespaceSymbol rootNamespace) + => [.. FindTypesToGenerateForNamespace(rootNamespace)]; + } +} \ No newline at end of file diff --git a/src/Tableau.Migration.PythonGenerator/PythonGeneratorService.cs b/src/Tableau.Migration.PythonGenerator/PythonGeneratorService.cs new file mode 100644 index 0000000..5615ea9 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonGeneratorService.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Tableau.Migration.PythonGenerator.Config; +using Tableau.Migration.PythonGenerator.Generators; +using Tableau.Migration.PythonGenerator.Writers; + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed class PythonGeneratorService : IHostedService + { + private readonly IHostApplicationLifetime _appLifetime; + private readonly PythonGeneratorOptions _options; + private readonly IPythonGenerator _pyGenerator; + private readonly IPythonWriter _pyWriter; + + public PythonGeneratorService(IHostApplicationLifetime appLifeTime, + IOptions options, + IPythonGenerator pyGenerator, + IPythonWriter pyWriter) + { + _appLifetime = appLifeTime; + _options = options.Value; + _pyGenerator = pyGenerator; + _pyWriter = pyWriter; + } + + public async Task StartAsync(CancellationToken cancel) + { + Console.WriteLine("Generating Python Wrappers..."); + Console.WriteLine("Import Path:" + _options.ImportPath); + Console.WriteLine("Output Path:" + _options.OutputPath); + + var compilation = CSharpCompilation.Create(typeof(ContentLocation).Assembly.GetName().Name) + .AddReferences(MetadataReference.CreateFromFile(Path.Combine(_options.ImportPath, "Tableau.Migration.dll"), + documentation: XmlDocumentationProvider.CreateFromFile(Path.Combine(_options.ImportPath, "Tableau.Migration.xml")))); + + var module = compilation.SourceModule.ReferencedAssemblySymbols.Single().Modules.Single(); + var rootNamespace = module.GlobalNamespace.GetNamespaceMembers().Single().GetNamespaceMembers().Single(); + + var dotNetTypes = PythonGenerationList.FindTypesToGenerate(rootNamespace); + + var pyTypes = _pyGenerator.Generate(dotNetTypes); + + await _pyWriter.WriteAsync(pyTypes, cancel); + + _appLifetime.StopApplication(); + } + + public Task StopAsync(CancellationToken cancel) => Task.CompletedTask; + } +} diff --git a/src/Tableau.Migration.PythonGenerator/PythonMethod.cs b/src/Tableau.Migration.PythonGenerator/PythonMethod.cs new file mode 100644 index 0000000..968d2c4 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonMethod.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed record PythonMethod(string Name, PythonTypeReference? ReturnType, ImmutableArray Arguments, + bool IsStatic, PythonDocstring? Documentation, IMethodSymbol DotNetMethod) + { } +} diff --git a/src/Tableau.Migration.PythonGenerator/PythonMethodArgument.cs b/src/Tableau.Migration.PythonGenerator/PythonMethodArgument.cs new file mode 100644 index 0000000..a0972d8 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonMethodArgument.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed record PythonMethodArgument(string Name, PythonTypeReference Type) + { } +} diff --git a/src/Tableau.Migration/Engine/Migrators/IContentMigrationHook.cs b/src/Tableau.Migration.PythonGenerator/PythonProperty.cs similarity index 69% rename from src/Tableau.Migration/Engine/Migrators/IContentMigrationHook.cs rename to src/Tableau.Migration.PythonGenerator/PythonProperty.cs index 1e5858d..0d076fb 100644 --- a/src/Tableau.Migration/Engine/Migrators/IContentMigrationHook.cs +++ b/src/Tableau.Migration.PythonGenerator/PythonProperty.cs @@ -15,13 +15,11 @@ // limitations under the License. // -using Tableau.Migration.Engine.Hooks; +using Microsoft.CodeAnalysis; -namespace Tableau.Migration.Engine.Migrators +namespace Tableau.Migration.PythonGenerator { - /// - /// Interface representing a hook called for a single content item. - /// - public interface IContentMigrationHook : IMigrationHook> + internal sealed record PythonProperty(string Name, PythonTypeReference Type, bool Getter, bool Setter, + PythonDocstring? Documentation, IPropertySymbol DotNetProperty) { } } diff --git a/src/Tableau.Migration.PythonGenerator/PythonType.cs b/src/Tableau.Migration.PythonGenerator/PythonType.cs new file mode 100644 index 0000000..28b4f04 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonType.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed record PythonType(string Name, string Module, + ImmutableArray InheritedTypes, + ImmutableArray Properties, ImmutableArray Methods, ImmutableArray EnumValues, + PythonDocstring? Documentation, INamedTypeSymbol DotNetType, ImmutableArray ExcludedInterfaces) + : IEquatable + { + public IEnumerable GetAllTypeReferences() + { + //Top-level Dotnet type ref + if(!DotNetType.IsAnyEnum()) + { + yield return new PythonTypeReference(DotNetType.Name, ImportModule: DotNetType.ContainingNamespace.ToDisplayString(), ConversionMode.Wrap); + } + + foreach(var t in InheritedTypes) + { + yield return t; + } + + foreach(var m in GetMemberTypeReferences()) + { + yield return m; + } + } + + public IEnumerable GetMemberTypeReferences() + { + foreach (var p in Properties) + { + yield return p.Type; + } + + foreach (var m in Methods) + { + if (m.ReturnType is not null) + { + yield return m.ReturnType; + } + + foreach (var arg in m.Arguments) + { + yield return arg.Type; + } + } + } + + public bool Equals(PythonTypeReference? other) + { + if(other is null) + { + return false; + } + + if(!string.Equals(Name, other.Name, System.StringComparison.Ordinal)) + { + return false; + } + + if (!string.Equals(Module, other.ImportModule, System.StringComparison.Ordinal)) + { + return false; + } + + return true; + } + + public bool HasSelfTypeReference() + => GetMemberTypeReferences().Any(Equals); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/PythonTypeCache.cs b/src/Tableau.Migration.PythonGenerator/PythonTypeCache.cs new file mode 100644 index 0000000..1402cfb --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonTypeCache.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed class PythonTypeCache + { + public ImmutableArray Types { get; } + + public PythonTypeCache(IEnumerable types) + { + Types = types.ToImmutableArray(); + } + + public PythonType? Find(PythonTypeReference r) + => Types.FirstOrDefault(t => t.Equals(r)); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/PythonTypeReference.cs b/src/Tableau.Migration.PythonGenerator/PythonTypeReference.cs new file mode 100644 index 0000000..a3c52a5 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/PythonTypeReference.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator +{ + internal sealed record PythonTypeReference(string Name, string? ImportModule, + ConversionMode ConversionMode, ImmutableArray? GenericTypes = null, + string? WrapType = null, string? DotNetParseFunction = null, + ImmutableArray? ExtraImports = null, + ImmutableArray? DotnetTypes = null, + string? ImportAlias = null + ) + { + public string GenericDefinitionName + => GenericTypes is null ? Name : $"{Name}[{string.Join(", ", GenericTypes.Value.Select(g => g.GenericDefinitionName))}]"; + + public bool IsExplicitReference => Name.Contains("."); + + private static string ToPythonTypeName(ITypeSymbol dotNetType) + { + var typeName = dotNetType.Name; + if (typeName.StartsWith("I")) + typeName = typeName.Substring(1); + + return "Py" + typeName; + } + + public IEnumerable UnwrapGenerics() + { + yield return this; + + if (GenericTypes is not null) + { + foreach (var generic in GenericTypes) + { + yield return generic; + } + } + } + + public static PythonTypeReference ForGenericType(ITypeSymbol genericType) + => new PythonTypeReference(genericType.Name, null, ConversionMode.WrapGeneric); + + public static PythonTypeReference ForDotNetType(ITypeSymbol dotNetType) + { + var typeName = ToPythonTypeName(dotNetType); + + var mode = ConversionMode.Wrap; + + ImmutableArray? genericTypes = null; + if (dotNetType is INamedTypeSymbol namedType) + { + if (namedType.TypeArguments.Any()) + { + genericTypes = namedType.TypeArguments.Select(ForGenericType).ToImmutableArray(); + } + + if (namedType.IsOrdinalEnum()) + { + mode = ConversionMode.Enum; + } + } + + return new(typeName, dotNetType.ToPythonModuleName(), mode, genericTypes); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/README.md b/src/Tableau.Migration.PythonGenerator/README.md new file mode 100644 index 0000000..bf8f88d --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/README.md @@ -0,0 +1,24 @@ +# Migration SDK Python Generator + +## About + +The Migration SDK Python generator creates wrapper code for a selection of C# types. +A custom generator is used due to the bespoke nature of the Python wrappers. +The generated code is designed to work with a subset of types and allow for manual wrapping of types that are difficult to wrap or pre-date the generator. + +## Usage + +The generator automatically runs when built, and is designed to output as part of local and scripted builds. +To add a new type to generate wrappers for, update the list in [`PythonGenerationList.cs`](PythonGenerationList.cs) + +### Hints + +The [`appsettings.json`](appsettings.json) file contains hints for the generator on a per-namespace/per-type basis: + +- `excludedMembers` type hint: An array of strings for member names (methods or properties) not not generate wrappers for. + +## Limitations + +The current generator has the following limitations that would need to be addressed to generate code for types making use of these features: + +- Overloaded methods diff --git a/src/Tableau.Migration.PythonGenerator/StringExtensions.cs b/src/Tableau.Migration.PythonGenerator/StringExtensions.cs new file mode 100644 index 0000000..895cf70 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/StringExtensions.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Tableau.Migration.PythonGenerator +{ + internal static class StringExtensions + { + [return: NotNullIfNotNull(nameof(s))] + public static string? ToSnakeCase(this string? s) + { + if(string.IsNullOrEmpty(s)) + { + return s; + } + + var sb = new StringBuilder(); + sb.Append(char.ToLowerInvariant(s[0])); + for (int i = 1; i < s.Length; ++i) + { + char c = s[i]; + if (char.IsUpper(c)) + { + sb.Append('_'); + sb.Append(char.ToLowerInvariant(c)); + } + else + { + sb.Append(c); + } + } + return sb.ToString(); + } + + [return: NotNullIfNotNull(nameof(s))] + public static string? ToConstantCase(this string? s) + => ToSnakeCase(s)?.ToUpper(); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Tableau.Migration.PythonGenerator.csproj b/src/Tableau.Migration.PythonGenerator/Tableau.Migration.PythonGenerator.csproj new file mode 100644 index 0000000..d26d1ba --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Tableau.Migration.PythonGenerator.csproj @@ -0,0 +1,20 @@ + + + Tableau Migration SDK Python Wrapper Generator + Exe + net8.0 + + CA2007 + + + + + + + + + + + + + diff --git a/src/Tableau.Migration.PythonGenerator/Writers/GeneratedPythonSegment.cs b/src/Tableau.Migration.PythonGenerator/Writers/GeneratedPythonSegment.cs new file mode 100644 index 0000000..c773211 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/GeneratedPythonSegment.cs @@ -0,0 +1,104 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal sealed class GeneratedPythonSegment : IAsyncDisposable + { + private const string BEGIN_REGION = "# region _generated"; + private const string END_REGION = "# endregion"; + + private readonly string _path; + private readonly string? _tailContent; + + public IndentingStringBuilder StringBuilder { get; } + + private GeneratedPythonSegment(string path, IndentingStringBuilder stringBuilder, string? tailContent) + { + _path = path; + StringBuilder = stringBuilder; + _tailContent = tailContent; + } + + private static async ValueTask<(IndentingStringBuilder StringBuilder, string? TailContent)> InitializeAsync(Stream stream, CancellationToken cancel) + { + using var reader = new StreamReader(stream, leaveOpen: true); + var text = await reader.ReadToEndAsync(cancel); + + var newLine = text.Contains("\r") ? "\r\n" : "\n"; + + IndentingStringBuilder sb = new(new(), newLine); + string? tailContent = null; + + var regionStartIndex = text.IndexOf(BEGIN_REGION); + if(regionStartIndex == -1) + { + sb.NonIndentingBuilder.Append(text); + } + else + { + if(regionStartIndex > 0) + { + sb.NonIndentingBuilder.Append(text.Substring(0, regionStartIndex)); + } + + var regionEndIndex = text.IndexOf(END_REGION, regionStartIndex); + if(regionEndIndex != -1) + { + var tailIndex = regionEndIndex + END_REGION.Length; + if(tailIndex < text.Length - 1) + { + tailContent = text.Substring(tailIndex).TrimStart(); + } + } + } + + sb.AppendLine(BEGIN_REGION); + sb.AppendLine(); + + return (sb, tailContent); + } + + public static async ValueTask OpenAsync(string path, CancellationToken cancel) + { + await using var stream = File.Open(path, FileMode.OpenOrCreate, FileAccess.Read); + + var init = await InitializeAsync(stream, cancel); + + return new GeneratedPythonSegment(path, init.StringBuilder, init.TailContent); + } + + public async ValueTask DisposeAsync() + { + StringBuilder.AppendLine(); + StringBuilder.AppendLine(END_REGION); + + if (_tailContent is not null) + { + StringBuilder.AppendLine(); + StringBuilder.NonIndentingBuilder.Append(_tailContent); + } + + await File.WriteAllTextAsync(_path, StringBuilder.ToString()); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonClassTestWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonClassTestWriter.cs new file mode 100644 index 0000000..4f5a790 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonClassTestWriter.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonClassTestWriter + { + void Write(IndentingStringBuilder builder, PythonType type); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonConstructorTestWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonConstructorTestWriter.cs new file mode 100644 index 0000000..faf1fdc --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonConstructorTestWriter.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonConstructorTestWriter + { + void Write(IndentingStringBuilder builder, PythonType type); + } +} \ No newline at end of file diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonDocstringWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonDocstringWriter.cs new file mode 100644 index 0000000..89dd296 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonDocstringWriter.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonDocstringWriter + { + void Write(IndentingStringBuilder builder, PythonDocstring? documentation); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonEnumValueWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonEnumValueWriter.cs new file mode 100644 index 0000000..6ad1e09 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonEnumValueWriter.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonEnumValueWriter + { + void Write(IndentingStringBuilder builder, PythonType type, PythonEnumValue enumValue); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonMethodWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonMethodWriter.cs new file mode 100644 index 0000000..af0c3fe --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonMethodWriter.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonMethodWriter + { + void Write(IndentingStringBuilder builder, PythonType type, PythonMethod method); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonPropertyTestWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonPropertyTestWriter.cs new file mode 100644 index 0000000..1a3969d --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonPropertyTestWriter.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonPropertyTestWriter + { + void Write(IndentingStringBuilder builder, PythonType type, PythonProperty property); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonPropertyWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonPropertyWriter.cs new file mode 100644 index 0000000..ae80c79 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonPropertyWriter.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonPropertyWriter + { + void Write(IndentingStringBuilder builder, PythonType type, PythonProperty property); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonTestWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonTestWriter.cs new file mode 100644 index 0000000..f0d3bdb --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonTestWriter.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using System.Threading; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonTestWriter + { + ValueTask WriteAsync(PythonTypeCache pyTypeCache, CancellationToken cancel); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonTypeWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonTypeWriter.cs new file mode 100644 index 0000000..57e726c --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonTypeWriter.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonTypeWriter + { + void Write(IndentingStringBuilder builder, PythonType type); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/IPythonWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/IPythonWriter.cs new file mode 100644 index 0000000..238b4b7 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/IPythonWriter.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading; +using System.Threading.Tasks; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal interface IPythonWriter + { + ValueTask WriteAsync(PythonTypeCache pyTypeCache, CancellationToken cancel); + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/Imports/ImportedModule.cs b/src/Tableau.Migration.PythonGenerator/Writers/Imports/ImportedModule.cs new file mode 100644 index 0000000..ff928c5 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/Imports/ImportedModule.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; + +namespace Tableau.Migration.PythonGenerator.Writers.Imports +{ + internal class ImportedModule + { + public ImportedModule(string name, HashSet types) + { + Name = name; + Types = types; + } + public ImportedModule(string name, ImportedType type) + { + Name = name; + Types = [type]; + } + public string Name { get; set; } + + public HashSet Types { get; set; } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/Imports/ImportedType.cs b/src/Tableau.Migration.PythonGenerator/Writers/Imports/ImportedType.cs new file mode 100644 index 0000000..cda7619 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/Imports/ImportedType.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Tableau.Migration.PythonGenerator.Writers.Imports +{ + internal class ImportedType + { + public ImportedType(string name, string? alias = null) + { + Name = name; + Alias = alias; + } + + public string Name { get; set; } + + public string? Alias { get; set; } + + public string GetNameWithAlias() + { + return string.IsNullOrEmpty(Alias) ? Name : $"{Name} as {Alias}"; + } + + public override bool Equals(object? obj) + { + return obj is ImportedType importedType && + Name == importedType.Name && + Alias == importedType.Alias; + } + + public override int GetHashCode() + { + return HashCode.Combine(Name, Alias); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonClassTestWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonClassTestWriter.cs new file mode 100644 index 0000000..82053a0 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonClassTestWriter.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal class PythonClassTestWriter : IPythonClassTestWriter + { + private readonly IPythonPropertyTestWriter _propertyTestWriter; + private readonly IPythonConstructorTestWriter _constructorTestWriter; + + public PythonClassTestWriter( + IPythonConstructorTestWriter constructorTestWriter, + IPythonPropertyTestWriter propertyTestWriter) + { + _propertyTestWriter = propertyTestWriter; + _constructorTestWriter = constructorTestWriter; + } + + public void Write(IndentingStringBuilder builder, PythonType type) + { + if (!type.Properties.Any() & !type.Methods.Any()) + { + return; + } + + using var classBuilder = builder.AppendLineAndIndent($"class Test{type.Name}Generated(AutoFixtureTestBase):"); + classBuilder.AppendLine(); + + if (!type.EnumValues.Any()) + { + _constructorTestWriter.Write(classBuilder, type); + + foreach (var property in type.Properties) + { + _propertyTestWriter.Write(classBuilder, type, property); + } + } + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonConstructorTestWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonConstructorTestWriter.cs new file mode 100644 index 0000000..cf9b00b --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonConstructorTestWriter.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal sealed class PythonConstructorTestWriter : PythonMemberWriter, IPythonConstructorTestWriter + { + public PythonConstructorTestWriter() + { } + + public void Write(IndentingStringBuilder builder, PythonType type) + { + using (var ctorBuilder = builder.AppendLineAndIndent($"def test_ctor(self):")) + { + BuildCtorTestBody(type, ctorBuilder); + } + + builder.AppendLine(); + } + + private static void BuildCtorTestBody(PythonType type, IndentingStringBuilder ctorBuilder) + { + var dotnetObj = "dotnet"; + var pyObj = "py"; + + ctorBuilder.AppendLine($"{dotnetObj} = self.create({type.DotNetType.Name})"); + ctorBuilder.AppendLine($"{pyObj} = {type.Name}({dotnetObj})"); + ctorBuilder.AppendLine($"assert {pyObj}._dotnet == {dotnetObj}"); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonDocstringWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonDocstringWriter.cs new file mode 100644 index 0000000..d9f8125 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonDocstringWriter.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal sealed class PythonDocstringWriter : IPythonDocstringWriter + { + private const string DOCSTRING_QUOTE = "\"\"\""; + + public void Write(IndentingStringBuilder builder, PythonDocstring? documentation) + { + if (documentation is null) + { + return; + } + + if (!documentation.HasExtraInfo) + { + builder.AppendLine($"{DOCSTRING_QUOTE}{documentation.Summary}{DOCSTRING_QUOTE}"); + return; + } + + builder.AppendLine($"{DOCSTRING_QUOTE}{documentation.Summary}"); + + if (documentation.HasArgs) + { + builder.AppendLine(); + using var argsBuilder = builder.AppendLineAndIndent("Args:"); + foreach (var arg in documentation.Args) + { + argsBuilder.AppendLine($"{arg.Name}: {arg.Documentation}"); + } + } + + if (documentation.HasReturns) + { + builder.AppendLine(); + builder.AppendLine($"Returns: {documentation.Returns}"); + } + + builder.AppendLine(DOCSTRING_QUOTE); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonEnumValueWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonEnumValueWriter.cs new file mode 100644 index 0000000..a725aaa --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonEnumValueWriter.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal class PythonEnumValueWriter : PythonMemberWriter, IPythonEnumValueWriter + { + private readonly IPythonDocstringWriter _docWriter; + + public PythonEnumValueWriter(IPythonDocstringWriter docWriter) + { + _docWriter = docWriter; + } + + public void Write(IndentingStringBuilder builder, PythonType type, PythonEnumValue enumValue) + { + _docWriter.Write(builder, enumValue.Documentation); + + string enumValueToken; + if(enumValue.Value is string) + { + enumValueToken = $"\"{enumValue.Value}\""; + } + else + { + enumValueToken = enumValue.Value.ToString() ?? string.Empty; + } + + builder.AppendLine($"{enumValue.Name} = {enumValueToken}"); + builder.AppendLine(); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonMemberWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonMemberWriter.cs new file mode 100644 index 0000000..2414c2e --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonMemberWriter.cs @@ -0,0 +1,132 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal abstract class PythonMemberWriter + { + protected static string ToPythonTypeDeclaration(PythonType currentType, PythonTypeReference typeRef) + { + if (currentType.Equals(typeRef)) + { + return "Self"; + } + + if (typeRef.GenericTypes is null) + { + return typeRef.Name; + } + + return $"{typeRef.Name}[{string.Join(", ", typeRef.GenericTypes.Value.Select(g => ToPythonTypeDeclaration(currentType, g)))}]"; + } + + private static string BuildWrapExpression(string nullTestExpression, string wrapExpression) + => $"None if {nullTestExpression} is None else {wrapExpression}"; + + private static string BuildWrapExpression(string nullTestExpression, string wrapFunction, string wrapExpression) + => $"None if {nullTestExpression} is None else {wrapFunction}({wrapExpression})"; + + private static string BuildArrayWrapExpression(string expression, string? itemName = null) + { + var arrayElement = string.IsNullOrEmpty(itemName) ? $"x" : $"{itemName}(x)"; + return $"[] if {expression} is None else [{arrayElement} for x in {expression} if x is not None]"; + } + + + protected static string ToPythonType(PythonTypeReference typeRef, string expression) + { + var wrapCtor = typeRef.WrapType ?? typeRef.GenericDefinitionName; + + switch (typeRef.ConversionMode) + { + case ConversionMode.Wrap: + return BuildWrapExpression(expression, wrapCtor, expression); + case ConversionMode.WrapSerialized: + return BuildWrapExpression(expression, wrapCtor, $"{expression}.ToString()"); + case ConversionMode.WrapGeneric: + return BuildWrapExpression(expression, "_generic_wrapper", expression); + case ConversionMode.WrapImmutableCollection: + case ConversionMode.WrapArray: + if (typeRef.GenericTypes is null || !typeRef.GenericTypes.Value.Any()) + { + return BuildWrapExpression(expression, wrapCtor, expression); + } + + if (typeRef.GenericTypes.Value.Length > 1) + { + throw new InvalidOperationException("Multi-dimensional collections are not currently supported."); + } + + var itemType = typeRef.GenericTypes.Value[0]; + if (itemType.ConversionMode is ConversionMode.Direct) + { + return BuildWrapExpression(expression, wrapCtor, expression); + } + + var itemExpression = ToPythonType(itemType, "x"); + return BuildWrapExpression(expression, wrapCtor, $"({itemExpression}) for x in {expression}"); + case ConversionMode.WrapMutableCollection: + if (typeRef.GenericTypes is null || !typeRef.GenericTypes.Value.Any()) + { + return BuildArrayWrapExpression(expression); + } + + if (typeRef.GenericTypes.Value.Length > 1) + { + throw new InvalidOperationException("Multi-dimensional collections are not currently supported."); + } + + var mutableItemType = typeRef.GenericTypes.Value[0]; + + if (mutableItemType.ConversionMode is ConversionMode.Direct) + { + return BuildArrayWrapExpression(expression); + } + + return BuildArrayWrapExpression(expression, mutableItemType.Name); + case ConversionMode.Enum: + return BuildWrapExpression(expression, wrapCtor, $"{expression}.value__"); + case ConversionMode.Direct: + default: + return expression; + } + } + + protected static string ToDotNetType(PythonTypeReference typeRef, string expression, bool skipNoneCheck = false) + { + switch (typeRef.ConversionMode) + { + case ConversionMode.Wrap: + case ConversionMode.WrapImmutableCollection: + case ConversionMode.WrapMutableCollection: + case ConversionMode.WrapArray: + if (skipNoneCheck) + return $"{expression}.{PythonTypeWriter.DOTNET_OBJECT}"; + else + return BuildWrapExpression(expression, $"{expression}.{PythonTypeWriter.DOTNET_OBJECT}"); + case ConversionMode.WrapSerialized: + return BuildWrapExpression(expression, typeRef.DotNetParseFunction!, $"str({expression})"); + case ConversionMode.Direct: + default: + return expression; + } + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonMethodWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonMethodWriter.cs new file mode 100644 index 0000000..5c518f0 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonMethodWriter.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal sealed class PythonMethodWriter : PythonMemberWriter, IPythonMethodWriter + { + private readonly IPythonDocstringWriter _docWriter; + + public PythonMethodWriter(IPythonDocstringWriter docWriter) + { + _docWriter = docWriter; + } + + private string BuildArgumentDeclarations(PythonType type, PythonMethod method) + { + string ArgumentDeclaration(PythonMethodArgument arg) + => $"{arg.Name}: {ToPythonTypeDeclaration(type, arg.Type)}"; + + var prefix = method.IsStatic ? "cls" : "self"; + + return string.Join(", ", method.Arguments.Select(ArgumentDeclaration).Prepend(prefix)); + } + + private string BuildArgumentInvocation(PythonMethod method) + { + string ArgumentInvocation(PythonMethodArgument arg) + => ToDotNetType(arg.Type, arg.Name); + + return string.Join(", ", method.Arguments.Select(ArgumentInvocation)); + } + + public void Write(IndentingStringBuilder builder, PythonType type, PythonMethod method) + { + var returnTypeName = method.ReturnType is null? "None" : ToPythonTypeDeclaration(type, method.ReturnType); + + if(method.IsStatic) + { + builder.AppendLine("@classmethod"); + } + + var argDeclarations = BuildArgumentDeclarations(type, method); + using (var methodBuilder = builder.AppendLineAndIndent($"def {method.Name}({argDeclarations}) -> {returnTypeName}:")) + { + _docWriter.Write(methodBuilder, method.Documentation); + + var argInvocation = BuildArgumentInvocation(method); + var dotNetInvoker = method.IsStatic ? type.DotNetType.Name : $"self.{PythonTypeWriter.DOTNET_OBJECT}"; + var methodInvokeExpression = $"{dotNetInvoker}.{method.DotNetMethod.Name}({argInvocation})"; + + if(method.ReturnType is not null) + { + methodBuilder.AppendLine($"result = {methodInvokeExpression}"); + var returnExpression = ToPythonType(method.ReturnType, "result"); + methodBuilder.AppendLine($"return {returnExpression}"); + } + else + { + methodBuilder.AppendLine(methodInvokeExpression); + } + } + + builder.AppendLine(); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonPropertyTestWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonPropertyTestWriter.cs new file mode 100644 index 0000000..075824b --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonPropertyTestWriter.cs @@ -0,0 +1,184 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal sealed class PythonPropertyTestWriter : PythonMemberWriter, IPythonPropertyTestWriter + { + public PythonPropertyTestWriter() + { } + + public void Write(IndentingStringBuilder builder, PythonType type, PythonProperty property) + { + if (property.Getter) + { + using (var getterBuilder = builder.AppendLineAndIndent($"def test_{property.Name}_getter(self):")) + { + BuildGetterTestBody(type, property, getterBuilder); + } + + builder.AppendLine(); + } + + if (property.Setter) + { + using (var getterBuilder = builder.AppendLineAndIndent($"def test_{property.Name}_setter(self):")) + { + BuildSetterTestBody(type, property, getterBuilder); + } + + builder.AppendLine(); + } + + } + + private static void BuildSetterTestBody(PythonType type, PythonProperty property, IndentingStringBuilder getterBuilder) + { + var dotnetObj = "dotnet"; + var pyObj = "py"; + + getterBuilder.AppendLine($"{dotnetObj} = self.create({type.DotNetType.Name})"); + getterBuilder.AppendLine($"{pyObj} = {type.Name}({dotnetObj})"); + + AddSetterAsserts(getterBuilder, dotnetObj, pyObj, property); + } + + private static void AddSetterAsserts(IndentingStringBuilder builder, string dotnetObj, string pyObj, PythonProperty property) + { + string dotnetPropValue = $"{dotnetObj}.{property.DotNetProperty.Name}"; + var pythonPropValue = $"{pyObj}.{property.Name}"; + + var typeRef = property.Type; + + switch (typeRef.ConversionMode) + { + case ConversionMode.WrapImmutableCollection: + case ConversionMode.WrapMutableCollection: + case ConversionMode.WrapArray: + { + builder.AppendLine($"assert len({dotnetPropValue}) != 0"); + builder.AppendLine($"assert len({pythonPropValue}) == len({dotnetPropValue})"); + builder.AppendLine(); + + builder.AppendLine("# create test data"); + + var dotnetType = property.DotNetProperty.Type; + + var element = dotnetType switch + { + INamedTypeSymbol dotnetNameType => dotnetNameType.TypeArguments.First().Name, + IArrayTypeSymbol dotnetArrayType => dotnetArrayType.ElementType.Name, + _ => throw new System.InvalidOperationException($"{dotnetType} is not supported."), + }; + + builder.AppendLine($"dotnetCollection = DotnetList[{element}]()"); + + for (var i = 1; i < 3; i++) + { + builder = builder.AppendLine($"dotnetCollection.Add(self.create({element}))"); + } + + var collectionWrapExp = ToPythonType(typeRef, $"dotnetCollection"); + builder.AppendLine($"testCollection = {collectionWrapExp}"); + builder.AppendLine(); + + builder.AppendLine("# set property to new test value"); + builder.AppendLine($"{pythonPropValue} = testCollection"); + builder.AppendLine(); + + builder.AppendLine("# assert value"); + builder.AppendLine($"assert len({pythonPropValue}) == len(testCollection)"); + + break; + } + default: + { + builder.AppendLine(); + + builder.AppendLine("# create test data"); + + var dotnetType = (INamedTypeSymbol)property.DotNetProperty.Type; + + if (!dotnetType.IsGenericType) + { + builder.AppendLine($"testValue = self.create({dotnetType.Name})"); + } + else + { + var args = string.Join(", ", dotnetType.TypeArguments.Select(x => x.Name)); + builder.AppendLine($"testValue = self.create({dotnetType.OriginalDefinition.Name}[{args}])"); + } + + builder.AppendLine(); + + var wrapExp = ToPythonType(typeRef, "testValue"); + builder.AppendLine("# set property to new test value"); + builder.AppendLine($"{pythonPropValue} = {wrapExp}"); + builder.AppendLine(); + + builder.AppendLine("# assert value"); + builder.AppendLine($"assert {pythonPropValue} == {wrapExp}"); + break; + } + } + } + + private static void BuildGetterTestBody(PythonType type, PythonProperty property, IndentingStringBuilder getterBuilder) + { + var dotnetObj = "dotnet"; + var pyObj = "py"; + + getterBuilder.AppendLine($"{dotnetObj} = self.create({type.DotNetType.Name})"); + getterBuilder.AppendLine($"{pyObj} = {type.Name}({dotnetObj})"); + + AddGetterAsserts(getterBuilder, dotnetObj, pyObj, property); + } + + private static void AddGetterAsserts(IndentingStringBuilder builder, string dotnetObj, string pyObj, PythonProperty property) + { + string dotnetPropValue = $"{dotnetObj}.{property.DotNetProperty.Name}"; + var pythonPropValue = $"{pyObj}.{property.Name}"; + + var typeRef = property.Type; + + switch (typeRef.ConversionMode) + { + case ConversionMode.WrapImmutableCollection: + case ConversionMode.WrapMutableCollection: + case ConversionMode.WrapArray: + builder.AppendLine($"assert len({dotnetPropValue}) != 0"); + builder.AppendLine($"assert len({pythonPropValue}) == len({dotnetPropValue})"); + break; + case ConversionMode.Enum: + var enumWrapExp = ToPythonType(typeRef, dotnetPropValue); + builder.AppendLine($"assert {pythonPropValue}.value == ({enumWrapExp}).value"); + break; + case ConversionMode.Wrap: + case ConversionMode.WrapSerialized: + case ConversionMode.WrapGeneric: + case ConversionMode.Direct: + default: + var wrapExp = ToPythonType(typeRef, dotnetPropValue); + builder.AppendLine($"assert {pythonPropValue} == {wrapExp}"); + break; + } + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonPropertyWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonPropertyWriter.cs new file mode 100644 index 0000000..218cb3e --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonPropertyWriter.cs @@ -0,0 +1,171 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using Tableau.Migration.PythonGenerator.Generators; +using Py = Tableau.Migration.PythonGenerator.Keywords.Python; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal sealed class PythonPropertyWriter : PythonMemberWriter, IPythonPropertyWriter + { + private readonly IPythonDocstringWriter _docWriter; + + public PythonPropertyWriter(IPythonDocstringWriter docWriter) + { + _docWriter = docWriter; + } + + public void Write(IndentingStringBuilder builder, PythonType type, PythonProperty property) + { + var typeDeclaration = ToPythonTypeDeclaration(type, property.Type); + + if (property.Getter) + { + builder.AppendLine("@property"); + using (var getterBuilder = builder.AppendLineAndIndent($"def {property.Name}(self) -> {typeDeclaration}:")) + { + BuildGetterBody(property, getterBuilder); + } + + builder.AppendLine(); + } + + if (property.Setter) + { + builder.AppendLine($"@{property.Name}.setter"); + var paramName = "value"; + using (var setterBuilder = builder.AppendLineAndIndent($"def {property.Name}(self, {paramName}: {typeDeclaration}) -> None:")) + { + BuildSetterBody(property, setterBuilder, paramName); + } + + builder.AppendLine(); + } + } + + private void BuildGetterBody(PythonProperty property, IndentingStringBuilder getterBuilder) + { + _docWriter.Write(getterBuilder, property.Documentation); + + var getterExpression = ToPythonType(property.Type, $"self.{PythonTypeWriter.DOTNET_OBJECT}.{property.DotNetProperty.Name}"); + + getterBuilder.AppendLine($"return {getterExpression}"); + } + + private static void BuildForLoop(IndentingStringBuilder builder, string condition, Action body) + { + var forLoopBuilder = builder.AppendLineAndIndent($"for {condition}:"); + body(forLoopBuilder); + } + private static void BuildIfBlock(IndentingStringBuilder builder, string condition, Action body) + { + var ifBuilder = builder.AppendLineAndIndent($"if {condition}:"); + body(ifBuilder); + } + private static void BuildElseBlock(IndentingStringBuilder builder, Action body) + { + var elseBuilder = builder.AppendLineAndIndent($"else:"); + body(elseBuilder); + } + + private void BuildSetterBody(PythonProperty property, IndentingStringBuilder setterBuilder, string paramName) + { + _docWriter.Write(setterBuilder, property.Documentation); + + var conversionMode = property.Type.ConversionMode; + switch (conversionMode) + { + case ConversionMode.WrapMutableCollection: + case ConversionMode.WrapArray: + { + var typeRef = property.Type; + + var dotnetTypes = typeRef.DotnetTypes + ?? throw new InvalidOperationException("Dotnet types are necessary for wrapping mutable collections."); + var dotnetType = dotnetTypes[0]; + + var collectionTypeAlias = GetCollectionTypeAlias(conversionMode, typeRef.Name); + + BuildIfBlock(setterBuilder, $"{paramName} is None", (builder) => + { + builder.AppendLine($"self.{PythonTypeWriter.DOTNET_OBJECT}.{property.DotNetProperty.Name} = {collectionTypeAlias}[{dotnetType.Name}]()"); + }); + + BuildElseBlock(setterBuilder, (builder) => + { + builder.AppendLine($"dotnet_collection = {collectionTypeAlias}[{dotnetType.Name}]()"); + // Build for loop inside else block + var itemVariableName = "x"; + + BuildForLoop(builder, $"{itemVariableName} in filter(None,{paramName})", (builder) => + { + if (typeRef.GenericTypes is null || !typeRef.GenericTypes.Value.Any()) + { + builder.AppendLine($"dotnet_collection.Add({itemVariableName})"); + } + else if (typeRef.GenericTypes.Value.Length > 1) + { + throw new InvalidOperationException("Multi-dimensional collections are not currently supported."); + } + else + { + var element = ToDotNetType(typeRef.GenericTypes.Value[0], itemVariableName, skipNoneCheck: true); + builder.AppendLine($"dotnet_collection.Add({element})"); + } + }); + + if (conversionMode == ConversionMode.WrapArray) + { + builder.AppendLine($"self.{PythonTypeWriter.DOTNET_OBJECT}.{property.DotNetProperty.Name} = dotnet_collection.ToArray()"); + return; + } + builder.AppendLine($"self.{PythonTypeWriter.DOTNET_OBJECT}.{property.DotNetProperty.Name} = dotnet_collection"); + }); + break; + } + default: + { + var setterExpression = ToDotNetType(property.Type, paramName); + setterBuilder.AppendLine($"self.{PythonTypeWriter.DOTNET_OBJECT}.{property.DotNetProperty.Name} = {setterExpression}"); + break; + } + } + + static string? GetCollectionTypeAlias(ConversionMode conversionMode, string typeRefName) + { + string? collectionTypeAlias; + if (conversionMode == ConversionMode.WrapArray) + { + collectionTypeAlias = PythonMemberGenerator.LIST_REFERENCE.ImportAlias; + } + else + { + collectionTypeAlias = typeRefName switch + { + Py.Types.SEQUENCE => PythonMemberGenerator.HASH_SET_REFERENCE.ImportAlias, + _ => PythonMemberGenerator.LIST_REFERENCE.ImportAlias, + }; + } + + return collectionTypeAlias; + } + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonTestWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonTestWriter.cs new file mode 100644 index 0000000..9f1dd31 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonTestWriter.cs @@ -0,0 +1,289 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; +using Tableau.Migration.Content; +using Tableau.Migration.Content.Permissions; +using Tableau.Migration.PythonGenerator.Config; +using Dotnet= Tableau.Migration.PythonGenerator.Keywords.Dotnet; +using Py = Tableau.Migration.PythonGenerator.Keywords.Python; +using Tableau.Migration.PythonGenerator.Writers.Imports; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal sealed class PythonTestWriter : PythonWriterBase, IPythonTestWriter + { + private readonly PythonGeneratorOptions _options; + private readonly IPythonClassTestWriter _classTestWriter; + private readonly string _testDirectoryPath; + + public PythonTestWriter(IOptions options, IPythonClassTestWriter classTestWriter) + { + _options = options.Value; + _classTestWriter = classTestWriter; + _testDirectoryPath = Path.Combine(_options.OutputPath, "..", "..", "tests"); + } + + private bool SearchTypeHierarchy(PythonTypeCache typeCache, PythonType startType, Func search) + { + if (search(startType)) + { + return true; + } + + foreach (var parentTypeRef in startType.InheritedTypes) + { + var parentType = typeCache.Find(parentTypeRef); + if (parentType is not null && SearchTypeHierarchy(typeCache, parentType, search)) + { + return true; + } + } + + return false; + } + + private bool TypeHasMethod(PythonTypeCache typeCache, PythonType startType, IMethodSymbol searchMethod) + => SearchTypeHierarchy(typeCache, startType, t => t.Methods.Any(m => string.Equals(m.DotNetMethod.Name, searchMethod.Name, StringComparison.Ordinal))); + + private bool TypeHasProperty(PythonTypeCache typeCache, PythonType startType, IPropertySymbol searchProperty) + => SearchTypeHierarchy(typeCache, startType, t => t.Properties.Any(p => string.Equals(p.DotNetProperty.Name, searchProperty.Name, StringComparison.Ordinal))); + + private void AddHierarchyExcludedMemberHints(List excludedMembers, PythonTypeCache typeCache, PythonType type) + { + var typeHints = _options.Hints.ForType(type.DotNetType); + if (typeHints is not null && typeHints.ExcludeMembers.Any()) + { + excludedMembers.AddRange(typeHints.ExcludeMembers); + } + + foreach (var inheritedTypeRef in type.InheritedTypes) + { + var inheritedType = typeCache.Find(inheritedTypeRef); + if (inheritedType is not null) + { + AddHierarchyExcludedMemberHints(excludedMembers, typeCache, inheritedType); + } + } + } + + private string BuildExcludedMemberList(PythonTypeCache typeCache, PythonType t) + { + var excludedMembers = t.ExcludedInterfaces + .SelectMany(x => x.GetMembers()) + .Where(m => m is not IMethodSymbol method || (method.MethodKind is MethodKind.Ordinary && !TypeHasMethod(typeCache, t, method))) + .Where(m => m is not IPropertySymbol p || !TypeHasProperty(typeCache, t, p)) + .Select(x => x.Name) + .ToList(); + + // Async disposable interface doesn't return any members, but we don't implement dispose in our Python wrappers. + if (t.ExcludedInterfaces.Any(i => string.Equals(i.Name, "IAsyncDisposable", StringComparison.Ordinal))) + { + excludedMembers.Add("DisposeAsync"); + } + + AddHierarchyExcludedMemberHints(excludedMembers, typeCache, t); + + if (excludedMembers.Count == 0) + { + return "None"; + } + + var memberNames = excludedMembers + .Distinct() + .Order() + .Select(m => $"\"{m}\""); + + return $"[ {string.Join(", ", memberNames)} ]"; + } + + private static void WriteClassTestImports(IndentingStringBuilder builder, PythonTypeCache pyTypeCache) + { + var testTypes = pyTypeCache.Types; + + WritePythonImports(builder, testTypes); + + var enumDotNetTyeps = testTypes + .OrderBy(x => x.Module) + .Where(x => x.EnumValues.Any()) + .Select(x => x.DotNetType) + .OrderBy(x => x.ContainingNamespace.ToDisplayString()) + .ThenBy(x => x.Name) + .ToImmutableArray(); + + if (!enumDotNetTyeps.Any()) + { + return; + } + + foreach (var enumType in enumDotNetTyeps) + { + builder.AppendLine($"from {enumType.ContainingNamespace.ToDisplayString()} import {enumType.Name}"); + } + + builder.AppendLine(); + } + + private void WriteClassCompletenessTestData(IndentingStringBuilder builder, PythonTypeCache pyTypeCache) + { + var testClassTypes = pyTypeCache.Types + .Where(x => !x.EnumValues.Any()) + .OrderBy(x => x.Module) + .ThenBy(x => x.Name) + .ToImmutableArray(); + + using (var testDataBuilder = builder.AppendLineAndIndent("_generated_class_data = [")) + { + for (int i = 0; i < testClassTypes.Length; i++) + { + var type = testClassTypes[i]; + var suffix = i == testClassTypes.Length - 1 ? string.Empty : ","; + + testDataBuilder.AppendLine($"({type.Name}, {BuildExcludedMemberList(pyTypeCache, type)}){suffix}"); + } + } + + builder.AppendLine("]"); + builder.AppendLine(); + } + + private static void WriteEnumCompletenessTestData(IndentingStringBuilder builder, PythonTypeCache pyTypeCache) + { + var testEnumTypes = pyTypeCache.Types + .Where(x => x.EnumValues.Any()) + .OrderBy(x => x.Module) + .ThenBy(x => x.Name) + .ToImmutableArray(); + + using (var testDataBuilder = builder.AppendLineAndIndent("_generated_enum_data = [")) + { + for (int i = 0; i < testEnumTypes.Length; i++) + { + var type = testEnumTypes[i]; + var suffix = i == testEnumTypes.Length - 1 ? string.Empty : ","; + + testDataBuilder.AppendLine($"({type.Name}, {type.DotNetType.Name}){suffix}"); + } + } + + builder.AppendLine("]"); + } + + private async ValueTask WriteWrapperCompletenessTestDataAsync(PythonTypeCache pyTypeCache, CancellationToken cancel) + { + var testClassesPath = Path.Combine(_testDirectoryPath, "test_classes.py"); + await using var segment = await GeneratedPythonSegment.OpenAsync(testClassesPath, cancel); + + WriteClassTestImports(segment.StringBuilder, pyTypeCache); + WriteClassCompletenessTestData(segment.StringBuilder, pyTypeCache); + WriteEnumCompletenessTestData(segment.StringBuilder, pyTypeCache); + } + + private async ValueTask WriteContentTypeWrapperTestsAsync(PythonTypeCache pyTypeCache, CancellationToken cancel) + { + var extraTestImports = new List() + { + new(Dotnet.Namespaces.TABLEAU_MIGRATION,new ImportedType(nameof(IContentReference))), + new(Dotnet.Namespaces.SYSTEM, [new ImportedType(Dotnet.Types.BOOLEAN), new ImportedType(Dotnet.Types.NULLABLE)]) + }; + + await WriteTests($"{typeof(IUser).Namespace}", pyTypeCache, extraTestImports, cancel); + + } + + private async ValueTask WritePermissionsContentTypeWrapperTestsAsync(PythonTypeCache pyTypeCache, CancellationToken cancel) + { + var extraTestImports = new List() + { + new(Dotnet.Namespaces.TABLEAU_MIGRATION,new ImportedType(nameof(IContentReference))), + new(Dotnet.Namespaces.SYSTEM,new ImportedType(Dotnet.Types.NULLABLE)), + new(Dotnet.Namespaces.SYSTEM_COLLECTIONS_GENERIC, new ImportedType(Dotnet.Types.LIST,Dotnet.TypeAliases.LIST)) + }; + + await WriteTests($"{typeof(IPermissions).Namespace}", pyTypeCache, extraTestImports, cancel); + } + + private async Task WriteTests(string nameSpace, PythonTypeCache pyTypeCache, List extraTestImports, CancellationToken cancel) + { + var classes = new PythonTypeCache(pyTypeCache + .Types + .Where(x + => x.DotNetType?.ContainingNamespace?.ToDisplayString() != null + && x.DotNetType.ContainingNamespace.ToDisplayString() == nameSpace) + .ToArray()); + var moduleGroups = classes.Types.GroupBy(p => p.Module); + + foreach (var moduleGroup in moduleGroups) + { + var pyFileName = moduleGroup.Key.Replace("tableau_migration.", "") + ".py"; + var pyFilePath = Path.Combine(_testDirectoryPath, $"test_{pyFileName}"); + + await using var segment = await GeneratedPythonSegment.OpenAsync(pyFilePath, cancel); + + WriteImports(segment.StringBuilder, moduleGroup.Key, moduleGroup); + var pythonTypes = moduleGroup.ToImmutableArray(); + + WriteClassTestImports(segment.StringBuilder, new PythonTypeCache(pythonTypes)); + + WriteExtraTestImports(segment.StringBuilder, extraTestImports); + + foreach (var pythonType in pythonTypes) + { + _classTestWriter.Write(segment.StringBuilder, pythonType); + } + } + } + + private static void WriteExtraTestImports( + IndentingStringBuilder builder, + List extraTestImports) + { + builder.AppendLine("# Extra imports for tests."); + + var autoFixtureNamespace = Py.Modules.AUTOFIXTURE; + + if (!extraTestImports.Any(x => x.Name == autoFixtureNamespace)) + { + extraTestImports.Add(new ImportedModule(autoFixtureNamespace, new ImportedType(Py.Types.AUTOFIXTURE_TESTBASE))); + } + else + { + extraTestImports.First(x => x.Name == autoFixtureNamespace).Types.Add(new ImportedType(Py.Types.AUTOFIXTURE_TESTBASE)); + } + + foreach (var moduleImports in extraTestImports) + { + WriteModuleImports(builder, moduleImports); + } + builder.AppendLine(); + } + public async ValueTask WriteAsync(PythonTypeCache pyTypeCache, CancellationToken cancel) + { + await WriteWrapperCompletenessTestDataAsync(pyTypeCache, cancel); + await WriteContentTypeWrapperTestsAsync(pyTypeCache, cancel); + await WritePermissionsContentTypeWrapperTestsAsync(pyTypeCache, cancel); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonTypeWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonTypeWriter.cs new file mode 100644 index 0000000..e41a031 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonTypeWriter.cs @@ -0,0 +1,98 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Linq; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal class PythonTypeWriter : IPythonTypeWriter + { + internal const string DOTNET_OBJECT = "_dotnet"; + private const string DOTNET_BASE = "_dotnet_base"; + + private readonly IPythonDocstringWriter _docWriter; + private readonly IPythonPropertyWriter _propertyWriter; + private readonly IPythonMethodWriter _methodWriter; + private readonly IPythonEnumValueWriter _enumValueWriter; + + public PythonTypeWriter(IPythonDocstringWriter docWriter, + IPythonPropertyWriter propertyWriter, + IPythonMethodWriter methodWriter, + IPythonEnumValueWriter enumValueWriter) + { + _docWriter = docWriter; + _propertyWriter = propertyWriter; + _methodWriter = methodWriter; + _enumValueWriter = enumValueWriter; + } + + private static string ToParamName(string dotNetTypeName) + { + if(dotNetTypeName.StartsWith("I")) + dotNetTypeName = dotNetTypeName.Substring(1); + + return dotNetTypeName.ToSnakeCase(); + } + + public void Write(IndentingStringBuilder builder, PythonType type) + { + var inheritedTypeNames = type.InheritedTypes.Select(t => t.GenericDefinitionName); + var inheritedTypes = string.Join(", ", inheritedTypeNames); + + using (var classBuilder = builder.AppendLineAndIndent($"class {type.Name}({inheritedTypes}):")) + { + _docWriter.Write(classBuilder, type.Documentation); + classBuilder.AppendLine(); + + if(type.EnumValues.Any()) + { + foreach(var enumValue in type.EnumValues) + { + _enumValueWriter.Write(classBuilder, type, enumValue); + } + } + else + { + var dotNetType = type.DotNetType.Name; + + classBuilder.AppendLine($"{DOTNET_BASE} = {dotNetType}"); + classBuilder.AppendLine(); + + var dotNetParam = ToParamName(dotNetType); + using (var ctorBuilder = classBuilder.AppendLineAndIndent($"def __init__(self, {dotNetParam}: {dotNetType}) -> None:")) + { + var ctorDoc = new PythonDocstring($"Creates a new {type.Name} object.", "None.", (dotNetParam, $"A {dotNetType} object.")); + _docWriter.Write(ctorBuilder, ctorDoc); + + ctorBuilder.AppendLine($"self.{DOTNET_OBJECT} = {dotNetParam}"); + ctorBuilder.AppendLine(); + } + + foreach (var property in type.Properties) + { + _propertyWriter.Write(classBuilder, type, property); + } + + foreach (var method in type.Methods) + { + _methodWriter.Write(classBuilder, type, method); + } + } + } + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonWriter.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonWriter.cs new file mode 100644 index 0000000..7412828 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonWriter.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Tableau.Migration.PythonGenerator.Config; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal sealed class PythonWriter : PythonWriterBase, IPythonWriter + { + private readonly IPythonTypeWriter _typeWriter; + private readonly IPythonTestWriter _testWriter; + private readonly PythonGeneratorOptions _options; + + public PythonWriter(IPythonTypeWriter typeWriter, IPythonTestWriter testWriter, + IOptions options) + { + _typeWriter = typeWriter; + _testWriter = testWriter; + _options = options.Value; + } + + private static void WriteTypeVars(IndentingStringBuilder builder, IEnumerable pyTypes) + { + var typeVars = pyTypes + .SelectMany(pt => pt.InheritedTypes) + .SelectMany(it => it.GenericTypes ?? ImmutableArray.Empty) + .Select(gt => gt.Name) + .Distinct() + .Order() + .ToImmutableArray(); + + if (typeVars.Any()) + { + foreach (var typeVar in typeVars) + { + builder.AppendLine($"{typeVar} = TypeVar(\"{typeVar}\")"); + } + + builder.AppendLine(); + } + } + + private void WriteTypeAndDependencies(IndentingStringBuilder builder, PythonType type, + IReadOnlyDictionary moduleTypes, HashSet writtenTypes, HashSet cycleReferences) + { + if (cycleReferences.Contains(type)) + { + throw new Exception("Type dependency cycle detected. Consider implementing ordering or stubbing."); + } + + if (writtenTypes.Contains(type)) + { + return; + } + + cycleReferences.Add(type); + + var refTypes = type.GetAllTypeReferences().SelectMany(t => t.UnwrapGenerics()); + foreach (var refType in refTypes) + { + if (!string.Equals(type.Module, refType.ImportModule, StringComparison.Ordinal) || + string.Equals(type.Name, refType.Name, StringComparison.Ordinal)) + { + continue; + } + + if (!moduleTypes.TryGetValue(refType.Name, out var dependencyType)) + { + continue; + } + + WriteTypeAndDependencies(builder, dependencyType, moduleTypes, writtenTypes, cycleReferences); + } + + cycleReferences.Remove(type); + + _typeWriter.Write(builder, type); + writtenTypes.Add(type); + } + + private void WriteModuleTypes(IndentingStringBuilder builder, IEnumerable moduleTypes) + { + var moduleTypesByName = moduleTypes.ToImmutableDictionary(t => t.Name); + var writtenTypes = new HashSet(); + + foreach (var typeToWrite in moduleTypes.OrderBy(t => t.Name)) + { + var cycleReferences = new HashSet(); + WriteTypeAndDependencies(builder, typeToWrite, moduleTypesByName, writtenTypes, cycleReferences); + } + } + + private async Task WritePublicAliasesAsync(IEnumerable moduleTypes, CancellationToken cancel) + { + var initFilePath = Path.Combine(_options.OutputPath, "__init__.py"); + await using var segment = await GeneratedPythonSegment.OpenAsync(initFilePath, cancel); + + foreach (var type in moduleTypes.OrderBy(x => x.Module).ThenBy(x => x.Name)) + { + segment.StringBuilder.AppendLine($"from {type.Module} import {type.Name} as {type.DotNetType.Name} # noqa: E402, F401"); + } + } + + public async ValueTask WriteAsync(PythonTypeCache pyTypeCache, CancellationToken cancel) + { + var moduleGroups = pyTypeCache.Types + .GroupBy(p => p.Module); + + foreach (var moduleGroup in moduleGroups) + { + var pyFileName = moduleGroup.Key.Replace("tableau_migration.", "") + ".py"; + var pyFilePath = Path.Combine(_options.OutputPath, pyFileName); + + await using var segment = await GeneratedPythonSegment.OpenAsync(pyFilePath, cancel); + + WriteImports(segment.StringBuilder, moduleGroup.Key, moduleGroup); + + WriteTypeVars(segment.StringBuilder, moduleGroup); + + //Write types in dependency order. + WriteModuleTypes(segment.StringBuilder, moduleGroup); + } + + await WritePublicAliasesAsync(pyTypeCache.Types, cancel); + + await _testWriter.WriteAsync(pyTypeCache, cancel); + } + } +} diff --git a/src/Tableau.Migration.PythonGenerator/Writers/PythonWriterBase.cs b/src/Tableau.Migration.PythonGenerator/Writers/PythonWriterBase.cs new file mode 100644 index 0000000..5e681a7 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/Writers/PythonWriterBase.cs @@ -0,0 +1,171 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Tableau.Migration.PythonGenerator.Writers.Imports; + +namespace Tableau.Migration.PythonGenerator.Writers +{ + internal abstract class PythonWriterBase + { + protected internal static void WriteImports(IndentingStringBuilder builder, string currentModule, IEnumerable pyTypes) + { + var typeRefs = new HashSet(); + + foreach (var pyType in pyTypes) + { + foreach (var typeRef in pyType.GetAllTypeReferences()) + { + foreach (var t in typeRef.UnwrapGenerics()) + { + typeRefs.Add(t); + } + + if (typeRef.ExtraImports is not null) + { + foreach (var extraImport in typeRef.ExtraImports) + { + typeRefs.Add(extraImport); + } + } + } + } + + if (pyTypes.Any(t => t.HasSelfTypeReference())) + { + typeRefs.Add(new("Self", "typing_extensions", ConversionMode.Direct)); + } + + var pythonRefs = typeRefs.Where(IsPythonReference); + if (pythonRefs.Any()) + { + WriteImportSection(builder, currentModule, pythonRefs); + builder.AppendLine(); + } + + var dotNetRefs = typeRefs.Where(t => !IsPythonReference(t)); + if (dotNetRefs.Any()) + { + WriteImportSection(builder, currentModule, dotNetRefs); + builder.AppendLine(); + } + } + + protected internal static void WritePythonImports(IndentingStringBuilder builder, ImmutableArray pyTypes) + { + if (pyTypes.Length == 0) + { + return; + } + + if (pyTypes.Length == 1) + { + var typeName = pyTypes.First().Name; + var module = pyTypes.First().Module; + + builder.AppendLine($"from {module} import {typeName} # noqa: E402, F401"); + return; + } + + foreach (var moduleBasedGrouping in pyTypes.GroupBy(x => x.Module)) + { + var typesList = moduleBasedGrouping + .OrderBy(x => x.Name) + .Select(x => new ImportedType(x.Name)) + .ToHashSet(); + WriteModuleImports(builder, new ImportedModule(moduleBasedGrouping.Key, typesList)); + builder.AppendLine(); + } + + builder.AppendLine(); + } + + private static bool IsPythonReference(PythonTypeReference r) => string.IsNullOrEmpty(r.ImportModule) || char.IsLower(r.ImportModule[0]); + + private static void WriteImportSection(IndentingStringBuilder builder, string currentModule, IEnumerable typeRefs) + { + var explicitImportModules = typeRefs + .Where(r => r.IsExplicitReference && r.ImportModule is not null) + .Select(r => r.ImportModule!) + .Distinct() + .Order() + .ToImmutableArray(); + + if (explicitImportModules.Any()) + { + foreach (var explicitImportModule in explicitImportModules) + { + builder.AppendLine($"import {explicitImportModule} # noqa: E402"); + } + + builder.AppendLine(); + } + + var implicitImportModules = typeRefs + .Where(r => !r.IsExplicitReference) + .Where(r => r.ImportModule is not null && !string.Equals(currentModule, r.ImportModule, System.StringComparison.Ordinal)) + .GroupBy(r => r.ImportModule!) + .OrderBy(n => n.Key); + + foreach (var implicitImportModule in implicitImportModules) + { + var typesList = new HashSet(); + foreach (var kvp in implicitImportModule) + { + typesList.Add(new ImportedType(kvp.Name, kvp.ImportAlias)); + } + + WriteModuleImports(builder, new(implicitImportModule.Key, typesList)); + } + } + + /// + /// Writes imports for all types in a module using a list of type names. + /// + /// The string builder to use. + /// The name of the module. + /// The list of type names + internal static void WriteModuleImports(IndentingStringBuilder builder, ImportedModule importedModule) + { + var module = importedModule.Name; + var types = importedModule.Types; + if (types.Count == 0) + { + return; + } + + if (types.Count == 1) + { + builder.AppendLine($"from {module} import {types.First().GetNameWithAlias()} # noqa: E402, F401"); + return; + } + + var indentingBuilder = builder.AppendLineAndIndent($"from {module} import ( # noqa: E402, F401"); + + foreach (var typeName in types.SkipLast(1)) + { + indentingBuilder.AppendLine($"{typeName.GetNameWithAlias()},"); + } + + indentingBuilder.AppendLine($"{types.Last().GetNameWithAlias()}"); + builder.AppendLine(")"); + } + } +} \ No newline at end of file diff --git a/src/Tableau.Migration.PythonGenerator/appsettings.json b/src/Tableau.Migration.PythonGenerator/appsettings.json new file mode 100644 index 0000000..44bffb6 --- /dev/null +++ b/src/Tableau.Migration.PythonGenerator/appsettings.json @@ -0,0 +1,55 @@ +{ + "hints": { + "namespaces": [ + { + "namespace": "Tableau.Migration", + "types": [ + { + "type": "IResult", + "excludeMembers": [ "CastFailure" ] + } + ] + }, + { + "namespace": "Tableau.Migration.Engine.Hooks.Mappings", + "types": [ + { + "type": "ContentMappingContext", + "excludeMembers": [ "ToTask" ] + } + ] + }, + { + "namespace": "Tableau.Migration.Engine.Hooks.PostPublish", + "types": [ + { + "type": "BulkPostPublishContext", + "excludeMembers": [ "ToTask" ] + }, + { + "type": "ContentItemPostPublishContext", + "excludeMembers": [ "ToTask" ] + } + ] + }, + { + "namespace": "Tableau.Migration.Engine.Manifest", + "types": [ + { + "type": "IMigrationManifestEntryEditor", + "excludeMembers": [ "SetFailed" ] + } + ] + }, + { + "namespace": "Tableau.Migration.Content.Files", + "types": [ + { + "type": "IContentFileHandle", + "excludeMembers": [ "OpenReadAsync", "OpenWriteAsync", "GetXmlStreamAsync", "Store" ] + } + ] + } + ] + } +} diff --git a/src/Tableau.Migration/Api/Rest/Models/LicenseLevels.cs b/src/Tableau.Migration/Api/Rest/Models/LicenseLevels.cs index 59800c7..d86daa7 100644 --- a/src/Tableau.Migration/Api/Rest/Models/LicenseLevels.cs +++ b/src/Tableau.Migration/Api/Rest/Models/LicenseLevels.cs @@ -18,7 +18,7 @@ namespace Tableau.Migration.Api.Rest.Models { /// - /// The license levels to be dreived from a siterole in . + /// The license levels to be derived from a siterole in . /// public class LicenseLevels : StringEnum { diff --git a/src/Tableau.Migration/Api/Rest/Models/PermissionsCapabilityNames.cs b/src/Tableau.Migration/Api/Rest/Models/PermissionsCapabilityNames.cs index 062b239..f4c60a7 100644 --- a/src/Tableau.Migration/Api/Rest/Models/PermissionsCapabilityNames.cs +++ b/src/Tableau.Migration/Api/Rest/Models/PermissionsCapabilityNames.cs @@ -23,7 +23,7 @@ namespace Tableau.Migration.Api.Rest.Models public class PermissionsCapabilityNames : StringEnum { /// - /// Gets the name of capability name for no cabapibilities. + /// Gets the name of capability name for no capabilities. /// public const string None = "None"; diff --git a/src/Tableau.Migration/Content/IConnection.cs b/src/Tableau.Migration/Content/IConnection.cs index 300bef1..b9141ee 100644 --- a/src/Tableau.Migration/Content/IConnection.cs +++ b/src/Tableau.Migration/Content/IConnection.cs @@ -15,15 +15,20 @@ // limitations under the License. // -using Tableau.Migration.Api.Rest; +using System; namespace Tableau.Migration.Content { /// /// Interface for a content item's embedded connection. /// - public interface IConnection : IRestIdentifiable + public interface IConnection { + /// + /// Gets the unique identifier. + /// + Guid Id { get; } + /// /// Gets the connection type for the response. /// diff --git a/src/Tableau.Migration/Content/IPublishableWorkbook.cs b/src/Tableau.Migration/Content/IPublishableWorkbook.cs index f6bcf95..0bb5fa2 100644 --- a/src/Tableau.Migration/Content/IPublishableWorkbook.cs +++ b/src/Tableau.Migration/Content/IPublishableWorkbook.cs @@ -34,6 +34,6 @@ public interface IPublishableWorkbook : IWorkbookDetails, IFileContent, IConnect /// /// Gets the names of the views that should be hidden. /// - ISet HiddenViewNames { get; } + ISet HiddenViewNames { get; set; } } } diff --git a/src/Tableau.Migration/Content/IView.cs b/src/Tableau.Migration/Content/IView.cs index db2e81b..776f35b 100644 --- a/src/Tableau.Migration/Content/IView.cs +++ b/src/Tableau.Migration/Content/IView.cs @@ -18,7 +18,7 @@ namespace Tableau.Migration.Content { /// - /// Interface for view associated with the content item + /// Interface for view associated with the content item. /// public interface IView : IWithTags, IPermissionsContent { } diff --git a/src/Tableau.Migration/Content/IWorkbookDetails.cs b/src/Tableau.Migration/Content/IWorkbookDetails.cs index 71cf5c9..3006024 100644 --- a/src/Tableau.Migration/Content/IWorkbookDetails.cs +++ b/src/Tableau.Migration/Content/IWorkbookDetails.cs @@ -24,7 +24,9 @@ namespace Tableau.Migration.Content /// public interface IWorkbookDetails : IWorkbook, IChildPermissionsContent { - /// + /// + /// Gets the view metadata. + /// public IImmutableList Views { get; } } } diff --git a/src/Tableau.Migration/Content/Permissions/ICapability.cs b/src/Tableau.Migration/Content/Permissions/ICapability.cs index e121bae..49c534c 100644 --- a/src/Tableau.Migration/Content/Permissions/ICapability.cs +++ b/src/Tableau.Migration/Content/Permissions/ICapability.cs @@ -30,7 +30,7 @@ public interface ICapability public string Name { get; } /// - /// The capability mode from + /// The capability mode from . /// public string Mode { get; } } diff --git a/src/Tableau.Migration/Content/PublishableWorkbook.cs b/src/Tableau.Migration/Content/PublishableWorkbook.cs index 1920dbc..ef0eaef 100644 --- a/src/Tableau.Migration/Content/PublishableWorkbook.cs +++ b/src/Tableau.Migration/Content/PublishableWorkbook.cs @@ -35,7 +35,7 @@ internal sealed class PublishableWorkbook : WorkbookDetails, IPublishableWorkboo public IImmutableList Connections { get; } /// - public ISet HiddenViewNames { get; } = new HashSet(View.NameComparer); + public ISet HiddenViewNames { get; set; } = new HashSet(View.NameComparer); public PublishableWorkbook(IWorkbookDetails workbook, IImmutableList connections, IContentFileHandle file) : base(workbook) diff --git a/src/Tableau.Migration/Content/Search/ContentReferenceCacheBase.cs b/src/Tableau.Migration/Content/Search/ContentReferenceCacheBase.cs index fb54683..1d9584d 100644 --- a/src/Tableau.Migration/Content/Search/ContentReferenceCacheBase.cs +++ b/src/Tableau.Migration/Content/Search/ContentReferenceCacheBase.cs @@ -31,6 +31,7 @@ public abstract class ContentReferenceCacheBase : IContentReferenceCache private readonly Dictionary _idCache = new(); private readonly SemaphoreSlim _writeSemaphore = new(1, 1); + private bool _loaded = false; /// /// Gets the count of items in the cache. @@ -93,19 +94,24 @@ public abstract class ContentReferenceCacheBase : IContentReferenceCache return cachedResult; } - var searchResults = await searchAsync(search, cancel).ConfigureAwait(false); - foreach (var searchResult in searchResults) + if (!_loaded) { - _idCache[searchResult.Id] = searchResult; - _locationCache[searchResult.Location] = searchResult; + // Load the cache with list values just once + var searchResults = await searchAsync(search, cancel).ConfigureAwait(false); + foreach (var searchResult in searchResults) + { + _idCache[searchResult.Id] = searchResult; + _locationCache[searchResult.Location] = searchResult; + } + + _loaded = true; + + // Retry lookup now that this attempt populated. + if (cache.TryGetValue(search, out cachedResult)) + { + return cachedResult; + } } - - // Retry lookup now that this attempt populated. - if (cache.TryGetValue(search, out cachedResult)) - { - return cachedResult; - } - // No cached results. Retry individual search. cachedResult = await individualSearchAsync(search, cancel).ConfigureAwait(false); diff --git a/src/Tableau.Migration/ContentLocation.cs b/src/Tableau.Migration/ContentLocation.cs index 29c5bb3..374ca23 100644 --- a/src/Tableau.Migration/ContentLocation.cs +++ b/src/Tableau.Migration/ContentLocation.cs @@ -54,7 +54,6 @@ public ContentLocation(params string[] segments) : this((IEnumerable)segments) { } - /// /// Creates a new value. /// @@ -107,10 +106,27 @@ public ContentLocation(string pathSeparator, IEnumerable segments) /// /// The user/group domain. /// The user/group name. - /// + /// The newly created . public static ContentLocation ForUsername(string domain, string username) => new(ImmutableArray.Create(domain, username), Constants.DomainNameSeparator); + /// + /// Creates a new value from a string. + /// + /// The full path of the location. + /// The separator to use between segments in the location path. + /// The newly created . + public static ContentLocation FromPath( + string contentLocationPath, + string pathSeparator = Constants.PathSeparator) + => new( + contentLocationPath + .Split( + pathSeparator, + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .ToImmutableArray(), + pathSeparator); + /// /// Compares the current instance with another object of the same type and returns /// an integer that indicates whether the current instance precedes, follows, or @@ -140,7 +156,7 @@ public ContentLocation Append(string name) /// Creates a new with the last path segment replaced. /// /// The new name to replace the last path segment with. - /// The renamed + /// The renamed . public ContentLocation Rename(string newName) => new(Parent(), newName); diff --git a/src/Tableau.Migration/Engine/Endpoints/MigrationEndpointFactory.cs b/src/Tableau.Migration/Engine/Endpoints/MigrationEndpointFactory.cs index f130b2d..38b70a6 100644 --- a/src/Tableau.Migration/Engine/Endpoints/MigrationEndpointFactory.cs +++ b/src/Tableau.Migration/Engine/Endpoints/MigrationEndpointFactory.cs @@ -29,8 +29,8 @@ namespace Tableau.Migration.Engine.Endpoints public class MigrationEndpointFactory : IMigrationEndpointFactory { private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly ManifestDestinationContentReferenceFinderFactory _destinationFinderFactory; - private readonly ManifestSourceContentReferenceFinderFactory _sourceFinderFactory; + private readonly IDestinationContentReferenceFinderFactory _destinationFinderFactory; + private readonly ISourceContentReferenceFinderFactory _sourceFinderFactory; private readonly IContentFileStore _fileStore; private readonly ISharedResourcesLocalizer _localizer; @@ -43,8 +43,8 @@ public class MigrationEndpointFactory : IMigrationEndpointFactory /// The file store to use. /// A string localizer. public MigrationEndpointFactory(IServiceScopeFactory serviceScopeFactory, - ManifestSourceContentReferenceFinderFactory sourceFinderFactory, - ManifestDestinationContentReferenceFinderFactory destinationFinderFactory, + ISourceContentReferenceFinderFactory sourceFinderFactory, + IDestinationContentReferenceFinderFactory destinationFinderFactory, IContentFileStore fileStore, ISharedResourcesLocalizer localizer) { diff --git a/src/Tableau.Migration/Engine/Endpoints/Search/IDestinationContentReferenceFinder.cs b/src/Tableau.Migration/Engine/Endpoints/Search/IDestinationContentReferenceFinder.cs new file mode 100644 index 0000000..9656646 --- /dev/null +++ b/src/Tableau.Migration/Engine/Endpoints/Search/IDestinationContentReferenceFinder.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Threading; +using System.Threading.Tasks; +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Engine.Endpoints.Search +{ + /// + /// Interface for an object that can find destination content reference + /// for given content information, applying mapping rules. + /// + /// The content type. + public interface IDestinationContentReferenceFinder : IContentReferenceFinder + where TContent : IContentReference + { + /// + /// Finds the destination content reference for the source content reference location. + /// + /// The source content reference location. + /// A cancellation token to obey. + /// The found destination content reference, or null if no content reference was found. + Task FindBySourceLocationAsync(ContentLocation sourceLocation, CancellationToken cancel); + + /// + /// Finds the destination content reference for the mapped destination content reference location. + /// + /// The destination mapped content reference location. + /// A cancellation token to obey. + /// The found destination content reference, or null if no content reference was found. + Task FindByMappedLocationAsync(ContentLocation mappedLocation, CancellationToken cancel); + + /// + /// Finds the destination content reference for the source content reference unique identifier. + /// + /// The source content reference unique identifier. + /// A cancellation token to obey. + /// The found destination content reference, or null if no content reference was found. + Task FindBySourceIdAsync(Guid sourceId, CancellationToken cancel); + + /// + /// Finds the destination content reference for the source content reference URL. + /// + /// The source content reference URL. + /// A cancellation token to obey. + /// The found destination content reference, or null if no content reference was found. + Task FindBySourceContentUrlAsync(string sourceContentUrl, CancellationToken cancel); + } +} diff --git a/src/Tableau.Migration/Engine/Endpoints/Search/IDestinationContentReferenceFinderFactory.cs b/src/Tableau.Migration/Engine/Endpoints/Search/IDestinationContentReferenceFinderFactory.cs new file mode 100644 index 0000000..ef7b55a --- /dev/null +++ b/src/Tableau.Migration/Engine/Endpoints/Search/IDestinationContentReferenceFinderFactory.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Engine.Endpoints.Search +{ + /// + /// Interface for an object that can create destination content reference finders + /// based on content type. + /// + public interface IDestinationContentReferenceFinderFactory : IContentReferenceFinderFactory + { + /// + /// Gets or creates a destination content reference finder for a given content type. + /// + /// The content type. + /// The content reference finder. + IDestinationContentReferenceFinder ForDestinationContentType() + where TContent : class, IContentReference; + } +} diff --git a/src/Tableau.Migration/Engine/Endpoints/Search/IMappedContentReferenceFinder.cs b/src/Tableau.Migration/Engine/Endpoints/Search/IMappedContentReferenceFinder.cs deleted file mode 100644 index 4acac9d..0000000 --- a/src/Tableau.Migration/Engine/Endpoints/Search/IMappedContentReferenceFinder.cs +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright (c) 2024, Salesforce, Inc. -// SPDX-License-Identifier: Apache-2 -// -// Licensed under the Apache License, Version 2.0 (the "License") -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Tableau.Migration.Engine.Endpoints.Search -{ - /// - /// Interface for an object that can find equivalent destination content - /// given source content information, applying mapping rules. - /// - public interface IMappedContentReferenceFinder - { - /// - /// Finds the equivalent destination content reference for the source reference. - /// - /// The source content reference location. - /// A cancellation token to obey. - /// The found destination content reference, or null if no equivalent content exists. - Task FindDestinationReferenceAsync(ContentLocation sourceLocation, CancellationToken cancel); - - /// - /// Finds the destination content reference for a mapped destination location. - /// - /// The mapped content reference location. - /// A cancellation token to obey. - /// The found destination content reference, or null if no content was found. - Task FindMappedDestinationReferenceAsync(ContentLocation sourceLocation, CancellationToken cancel); - - /// - /// Finds the equivalent destination content reference for the source reference. - /// - /// The source content ID. - /// A cancellation token to obey. - /// The found destination content reference, or null if no equivalent content exists. - Task FindDestinationReferenceAsync(Guid sourceId, CancellationToken cancel); - - /// - /// Finds the equivalent destination content reference for the source reference. - /// - /// The source content URL. - /// A cancellation token to obey. - /// The found destination content reference, or null if no equivalent content exists. - Task FindDestinationReferenceAsync(string contentUrl, CancellationToken cancel); - } - - /// - /// Interface for an object that can find equivalent destination content - /// given source content information, applying mapping rules. - /// - /// The content type. - public interface IMappedContentReferenceFinder : IMappedContentReferenceFinder - { } -} diff --git a/src/Tableau.Migration/Engine/Endpoints/Search/ISourceContentReferenceFinder.cs b/src/Tableau.Migration/Engine/Endpoints/Search/ISourceContentReferenceFinder.cs new file mode 100644 index 0000000..04707b1 --- /dev/null +++ b/src/Tableau.Migration/Engine/Endpoints/Search/ISourceContentReferenceFinder.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using System.Threading; +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Engine.Endpoints.Search +{ + /// + /// Interface for an object that can find source content reference. + /// + /// The content type. + public interface ISourceContentReferenceFinder : IContentReferenceFinder + where TContent : IContentReference + { + /// + /// Finds the source content reference for the source content reference location. + /// + /// The source content reference location. + /// A cancellation token to obey. + /// The found source content reference, or null if no content reference was found. + Task FindBySourceLocationAsync(ContentLocation sourceLocation, CancellationToken cancel); + } +} diff --git a/src/Tableau.Migration/Engine/Endpoints/Search/ISourceContentReferenceFinderFactory.cs b/src/Tableau.Migration/Engine/Endpoints/Search/ISourceContentReferenceFinderFactory.cs new file mode 100644 index 0000000..13cebe6 --- /dev/null +++ b/src/Tableau.Migration/Engine/Endpoints/Search/ISourceContentReferenceFinderFactory.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Tableau.Migration.Content.Search; + +namespace Tableau.Migration.Engine.Endpoints.Search +{ + /// + /// Interface for an object that can create source content reference finders + /// based on content type. + /// + public interface ISourceContentReferenceFinderFactory : IContentReferenceFinderFactory + { + /// + /// Gets or creates a source content reference finder for a given content type. + /// + /// The content type. + /// The content reference finder. + ISourceContentReferenceFinder ForSourceContentType() + where TContent : class, IContentReference; + } +} diff --git a/src/Tableau.Migration/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinder.cs b/src/Tableau.Migration/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinder.cs index 5ff1c18..99ad08b 100644 --- a/src/Tableau.Migration/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinder.cs +++ b/src/Tableau.Migration/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinder.cs @@ -25,13 +25,13 @@ namespace Tableau.Migration.Engine.Endpoints.Search { /// - /// implementation + /// implementation /// that uses the mapped manifest information to find destination content, /// falling back to API loading. /// /// The content type. public class ManifestDestinationContentReferenceFinder - : IMappedContentReferenceFinder, IContentReferenceFinder + : IDestinationContentReferenceFinder where TContent : class, IContentReference { private readonly IMigrationManifestEditor _manifest; @@ -48,10 +48,10 @@ public ManifestDestinationContentReferenceFinder(IMigrationManifestEditor manife _destinationCache = pipeline.CreateDestinationCache(); } - #region - IMappedContentReferenceFinder Implementation - + #region - IDestinationContentReferenceFinder Implementation - /// - public async Task FindDestinationReferenceAsync(ContentLocation sourceLocation, CancellationToken cancel) + public async Task FindBySourceLocationAsync(ContentLocation sourceLocation, CancellationToken cancel) { //Get the DESTINATION reference for the SOURCE location. var manifestEntries = _manifest.Entries.GetOrCreatePartition(); @@ -69,7 +69,7 @@ public ManifestDestinationContentReferenceFinder(IMigrationManifestEditor manife } /// - public async Task FindMappedDestinationReferenceAsync(ContentLocation mappedLocation, CancellationToken cancel) + public async Task FindByMappedLocationAsync(ContentLocation mappedLocation, CancellationToken cancel) { //Get the DESTINATION reference for the DESTINATION location. var manifestEntries = _manifest.Entries.GetOrCreatePartition(); @@ -82,26 +82,26 @@ public ManifestDestinationContentReferenceFinder(IMigrationManifestEditor manife } /// - public async Task FindDestinationReferenceAsync(Guid sourceId, CancellationToken cancel) + public async Task FindBySourceIdAsync(Guid sourceId, CancellationToken cancel) { //Get the DESTINATION reference for the SOURCE ID. var manifestEntries = _manifest.Entries.GetOrCreatePartition(); if (manifestEntries.BySourceId.TryGetValue(sourceId, out var entry)) { - return await FindDestinationReferenceAsync(entry.Source.Location, cancel).ConfigureAwait(false); + return await FindBySourceLocationAsync(entry.Source.Location, cancel).ConfigureAwait(false); } return null; } /// - public async Task FindDestinationReferenceAsync(string contentUrl, CancellationToken cancel) + public async Task FindBySourceContentUrlAsync(string contentUrl, CancellationToken cancel) { //Get the DESTINATION reference for the SOURCE content URL. var manifestEntries = _manifest.Entries.GetOrCreatePartition(); if (manifestEntries.BySourceContentUrl.TryGetValue(contentUrl, out var entry)) { - return await FindDestinationReferenceAsync(entry.Source.Location, cancel).ConfigureAwait(false); + return await FindBySourceLocationAsync(entry.Source.Location, cancel).ConfigureAwait(false); } return null; @@ -109,7 +109,7 @@ public ManifestDestinationContentReferenceFinder(IMigrationManifestEditor manife #endregion - #region - IContentReferenceFinder Implementation - + #region - IContentReferenceFinder Implementation - /// public async Task FindByIdAsync(Guid id, CancellationToken cancel) diff --git a/src/Tableau.Migration/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderFactory.cs b/src/Tableau.Migration/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderFactory.cs index 333b7de..92cdeab 100644 --- a/src/Tableau.Migration/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderFactory.cs +++ b/src/Tableau.Migration/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderFactory.cs @@ -25,22 +25,28 @@ namespace Tableau.Migration.Engine.Endpoints.Search /// implementation that finds destination references /// from the migration manifest. /// - public class ManifestDestinationContentReferenceFinderFactory : IContentReferenceFinderFactory + public class ManifestDestinationContentReferenceFinderFactory + : IDestinationContentReferenceFinderFactory { private readonly IServiceProvider _services; /// /// Creates a new object. /// - /// A service provider. + /// A DI service provider to create finders with. public ManifestDestinationContentReferenceFinderFactory(IServiceProvider services) { _services = services; } /// - public IContentReferenceFinder ForContentType() + public virtual IContentReferenceFinder ForContentType() where TContent : class, IContentReference - => _services.GetRequiredService>(); + => ForDestinationContentType(); + + /// + public virtual IDestinationContentReferenceFinder ForDestinationContentType() + where TContent : class, IContentReference + => _services.GetRequiredService>(); } } diff --git a/src/Tableau.Migration/Engine/Endpoints/Search/ManifestSourceContentReferenceFinder.cs b/src/Tableau.Migration/Engine/Endpoints/Search/ManifestSourceContentReferenceFinder.cs index 5183008..b39bd67 100644 --- a/src/Tableau.Migration/Engine/Endpoints/Search/ManifestSourceContentReferenceFinder.cs +++ b/src/Tableau.Migration/Engine/Endpoints/Search/ManifestSourceContentReferenceFinder.cs @@ -29,7 +29,8 @@ namespace Tableau.Migration.Engine.Endpoints.Search /// from the migration manifest. /// /// The content type. - public class ManifestSourceContentReferenceFinder : IContentReferenceFinder + public class ManifestSourceContentReferenceFinder + : ISourceContentReferenceFinder where TContent : class, IContentReference { private readonly IMigrationManifestEditor _manifest; @@ -46,9 +47,23 @@ public ManifestSourceContentReferenceFinder(IMigrationManifestEditor manifest, I _sourceCache = pipeline.CreateSourceCache(); } + /// + public async Task FindBySourceLocationAsync(ContentLocation sourceLocation, CancellationToken cancel) + { + //Get the SOURCE reference for the SOURCE location. + var manifestEntries = _manifest.Entries.GetOrCreatePartition(); + if (manifestEntries.BySourceLocation.TryGetValue(sourceLocation, out var entry)) + { + return entry.Source; + } + + return await _sourceCache.ForLocationAsync(sourceLocation, cancel).ConfigureAwait(false); + } + /// public async Task FindByIdAsync(Guid id, CancellationToken cancel) { + //Get the SOURCE reference for the SOURCE ID. var partition = _manifest.Entries.GetOrCreatePartition(); if (partition.BySourceId.TryGetValue(id, out var entry)) diff --git a/src/Tableau.Migration/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderFactory.cs b/src/Tableau.Migration/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderFactory.cs index e314b4e..b171e0a 100644 --- a/src/Tableau.Migration/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderFactory.cs +++ b/src/Tableau.Migration/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderFactory.cs @@ -25,22 +25,27 @@ namespace Tableau.Migration.Engine.Endpoints.Search /// implementation that finds source references /// from the migration manifest. /// - public class ManifestSourceContentReferenceFinderFactory : IContentReferenceFinderFactory + public class ManifestSourceContentReferenceFinderFactory : ISourceContentReferenceFinderFactory { private readonly IServiceProvider _services; /// /// Creates a new object. /// - /// The service provider. + /// A DI service provider to create finders with. public ManifestSourceContentReferenceFinderFactory(IServiceProvider services) { _services = services; } /// - public IContentReferenceFinder ForContentType() + public virtual IContentReferenceFinder ForContentType() where TContent : class, IContentReference - => _services.GetRequiredService>(); + => ForSourceContentType(); + + /// + public virtual ISourceContentReferenceFinder ForSourceContentType() + where TContent : class, IContentReference + => _services.GetRequiredService>(); } } diff --git a/src/Tableau.Migration/Engine/Endpoints/TableauApiDestinationEndpoint.cs b/src/Tableau.Migration/Engine/Endpoints/TableauApiDestinationEndpoint.cs index 8854e09..f55a3fe 100644 --- a/src/Tableau.Migration/Engine/Endpoints/TableauApiDestinationEndpoint.cs +++ b/src/Tableau.Migration/Engine/Endpoints/TableauApiDestinationEndpoint.cs @@ -44,7 +44,7 @@ public class TableauApiDestinationEndpoint : TableauApiEndpointBase, IDestinatio /// A string localizer. public TableauApiDestinationEndpoint(IServiceScopeFactory serviceScopeFactory, ITableauApiEndpointConfiguration config, - ManifestDestinationContentReferenceFinderFactory finderFactory, + IDestinationContentReferenceFinderFactory finderFactory, IContentFileStore fileStore, ISharedResourcesLocalizer localizer) : base(serviceScopeFactory, config, finderFactory, fileStore, localizer) diff --git a/src/Tableau.Migration/Engine/Endpoints/TableauApiSourceEndpoint.cs b/src/Tableau.Migration/Engine/Endpoints/TableauApiSourceEndpoint.cs index 66565d2..ef08372 100644 --- a/src/Tableau.Migration/Engine/Endpoints/TableauApiSourceEndpoint.cs +++ b/src/Tableau.Migration/Engine/Endpoints/TableauApiSourceEndpoint.cs @@ -39,7 +39,7 @@ public class TableauApiSourceEndpoint : TableauApiEndpointBase, ISourceApiEndpoi /// A string localizer. public TableauApiSourceEndpoint(IServiceScopeFactory serviceScopeFactory, ITableauApiEndpointConfiguration config, - ManifestSourceContentReferenceFinderFactory finderFactory, + ISourceContentReferenceFinderFactory finderFactory, IContentFileStore fileStore, ISharedResourcesLocalizer localizer) : base(serviceScopeFactory, config, finderFactory, fileStore, localizer) diff --git a/src/Tableau.Migration/Engine/Hooks/Filters/ContentFilterBase.cs b/src/Tableau.Migration/Engine/Hooks/Filters/ContentFilterBase.cs index 8b8be32..f3f7272 100644 --- a/src/Tableau.Migration/Engine/Hooks/Filters/ContentFilterBase.cs +++ b/src/Tableau.Migration/Engine/Hooks/Filters/ContentFilterBase.cs @@ -48,7 +48,7 @@ public abstract class ContentFilterBase : IContentFilter { _localizer = localizer; _logger = logger; - _typeName = this.GetType().Name; + _typeName = GetType().Name; } /// diff --git a/src/Tableau.Migration/Engine/Hooks/Mappings/ContentMappingBase.cs b/src/Tableau.Migration/Engine/Hooks/Mappings/ContentMappingBase.cs index e9503e8..079e51a 100644 --- a/src/Tableau.Migration/Engine/Hooks/Mappings/ContentMappingBase.cs +++ b/src/Tableau.Migration/Engine/Hooks/Mappings/ContentMappingBase.cs @@ -17,7 +17,6 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Tableau.Migration.Resources; @@ -55,7 +54,7 @@ public abstract class ContentMappingBase : IContentMapping if((_logger is not null) && (_localizer is not null)) _logger.LogDebug(_localizer[(SharedResourceKeys.ContentMappingBaseDebugMessage)], _typeName, ctx.ContentItem.ToStringForLog(), ctx.MappedLocation); - + return ret; } diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/GroupUsersTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/GroupUsersTransformer.cs index 2b1343d..5898ece 100644 --- a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/GroupUsersTransformer.cs +++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/GroupUsersTransformer.cs @@ -19,7 +19,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Tableau.Migration.Content; -using Tableau.Migration.Engine.Pipelines; +using Tableau.Migration.Engine.Endpoints.Search; using Tableau.Migration.Resources; namespace Tableau.Migration.Engine.Hooks.Transformers.Default @@ -29,22 +29,22 @@ namespace Tableau.Migration.Engine.Hooks.Transformers.Default /// public class GroupUsersTransformer : ContentTransformerBase { - private readonly IMigrationPipeline _migrationPipeline; + private readonly IDestinationContentReferenceFinder _userFinder; private readonly ISharedResourcesLocalizer _localizer; private readonly ILogger _logger; /// /// Creates a new object. /// - /// Destination content finder object. + /// The destination finder factory. /// A string localizer. /// The logger used to log messages. public GroupUsersTransformer( - IMigrationPipeline migrationPipeline, + IDestinationContentReferenceFinderFactory destinationFinderFactory, ISharedResourcesLocalizer localizer, ILogger logger) : base(localizer, logger) { - _migrationPipeline = migrationPipeline; + _userFinder = destinationFinderFactory.ForDestinationContentType(); _localizer = localizer; _logger = logger; } @@ -54,12 +54,10 @@ public class GroupUsersTransformer : ContentTransformerBase IPublishableGroup sourceGroup, CancellationToken cancel) { - var userFinder = _migrationPipeline.CreateDestinationFinder(); - foreach (var user in sourceGroup.Users) { - var destinationUser = await userFinder - .FindDestinationReferenceAsync(user.User.Location, cancel) + var destinationUser = await _userFinder + .FindBySourceLocationAsync(user.User.Location, cancel) .ConfigureAwait(false); if (destinationUser is not null) diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/MappedUserTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/MappedUserTransformer.cs index a81431c..21d29c2 100644 --- a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/MappedUserTransformer.cs +++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/MappedUserTransformer.cs @@ -30,22 +30,22 @@ namespace Tableau.Migration.Engine.Hooks.Transformers.Default /// public class MappedUserTransformer : IMappedUserTransformer { - private readonly IMappedContentReferenceFinder _userFinder; + private readonly IDestinationContentReferenceFinder _userFinder; private readonly ILogger _logger; private readonly ISharedResourcesLocalizer _localizer; /// /// Creates a new object. /// - /// The migration pipeline. + /// The destination finder factory. /// The logger used to log messages. /// The string localizer. public MappedUserTransformer( - IMigrationPipeline pipeline, + IDestinationContentReferenceFinderFactory destinationFinderFactory, ILogger logger, ISharedResourcesLocalizer localizer) { - _userFinder = pipeline.CreateDestinationFinder(); + _userFinder = destinationFinderFactory.ForDestinationContentType(); _logger = logger; _localizer = localizer; } @@ -59,7 +59,7 @@ public class MappedUserTransformer : IMappedUserTransformer return null; } - var mapped = await _userFinder.FindDestinationReferenceAsync(ctx.Location, cancel) + var mapped = await _userFinder.FindBySourceLocationAsync(ctx.Location, cancel) .ConfigureAwait(false); if (mapped is null) diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/PermissionsTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/PermissionsTransformer.cs index 0911698..319391f 100644 --- a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/PermissionsTransformer.cs +++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/PermissionsTransformer.cs @@ -15,6 +15,7 @@ // limitations under the License. // +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -26,7 +27,6 @@ using Tableau.Migration.Content; using Tableau.Migration.Content.Permissions; using Tableau.Migration.Engine.Endpoints.Search; -using Tableau.Migration.Engine.Pipelines; using Tableau.Migration.Resources; namespace Tableau.Migration.Engine.Hooks.Transformers.Default @@ -36,21 +36,24 @@ namespace Tableau.Migration.Engine.Hooks.Transformers.Default /// public class PermissionsTransformer : IPermissionsTransformer { - private readonly IMappedContentReferenceFinder _userContentFinder; - private readonly IMappedContentReferenceFinder _groupContentFinder; + private readonly IDestinationContentReferenceFinder _userContentFinder; + private readonly IDestinationContentReferenceFinder _groupContentFinder; private readonly ILogger _logger; private readonly ISharedResourcesLocalizer _localizer; /// /// Creates a new object. /// - /// Destination content finder object. + /// The destination finder factory. /// Default logger. /// A string localizer. - public PermissionsTransformer(IMigrationPipeline migrationPipeline, ILogger logger, ISharedResourcesLocalizer localizer) + public PermissionsTransformer( + IDestinationContentReferenceFinderFactory destinationFinderFactory, + ILogger logger, + ISharedResourcesLocalizer localizer) { - _userContentFinder = migrationPipeline.CreateDestinationFinder(); - _groupContentFinder = migrationPipeline.CreateDestinationFinder(); + _userContentFinder = destinationFinderFactory.ForDestinationContentType(); + _groupContentFinder = destinationFinderFactory.ForDestinationContentType(); _logger = logger; _localizer = localizer; } @@ -91,11 +94,10 @@ private static bool ShouldMigrateCapability(ICapability c) { var granteeType = group.First().GranteeType; - IMappedContentReferenceFinder contentFinder = granteeType is GranteeType.User - ? _userContentFinder : _groupContentFinder; - - var destinationGrantee = await contentFinder - .FindDestinationReferenceAsync(group.Key, cancel) + var destinationGrantee = await GetDestinationGranteeAsync( + group.Key, + granteeType, + cancel) .ConfigureAwait(false); if (destinationGrantee is null) @@ -121,5 +123,22 @@ private static bool ShouldMigrateCapability(ICapability c) return transformedGrantees.ToImmutableArray(); } + + private async Task GetDestinationGranteeAsync( + Guid groupKey, + GranteeType granteeType, + CancellationToken cancel) + { + if (granteeType is GranteeType.User) + { + return await _userContentFinder + .FindBySourceIdAsync(groupKey, cancel) + .ConfigureAwait(false); + } + + return await _groupContentFinder + .FindBySourceIdAsync(groupKey, cancel) + .ConfigureAwait(false); + } } } diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/TableauServerConnectionUrlTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/TableauServerConnectionUrlTransformer.cs index 7fbecc6..107fa96 100644 --- a/src/Tableau.Migration/Engine/Hooks/Transformers/Default/TableauServerConnectionUrlTransformer.cs +++ b/src/Tableau.Migration/Engine/Hooks/Transformers/Default/TableauServerConnectionUrlTransformer.cs @@ -55,7 +55,7 @@ public class TableauServerConnectionUrlTransformer : XmlContentTransformerBase _mappedDataSourceFinder; + private readonly IDestinationContentReferenceFinder _mappedDataSourceFinder; private readonly ILogger _logger; private readonly ISharedResourcesLocalizer _localizer; @@ -69,15 +69,18 @@ public class TableauServerConnectionUrlTransformer : XmlContentTransformerBase object. /// /// The current migration. + /// The destination finder factory. /// A logger to use. /// A localizer to user. - public TableauServerConnectionUrlTransformer(IMigration migration, + public TableauServerConnectionUrlTransformer( + IMigration migration, + IDestinationContentReferenceFinderFactory destinationFinderFactory, ILogger logger, ISharedResourcesLocalizer localizer) { _contentUrlWarnings = new(StringComparer.OrdinalIgnoreCase); _destinationConfig = migration.Plan.Destination as ITableauApiEndpointConfiguration; - _mappedDataSourceFinder = migration.Pipeline.CreateDestinationFinder(); + _mappedDataSourceFinder = destinationFinderFactory.ForDestinationContentType(); _logger = logger; _localizer = localizer; } @@ -94,7 +97,7 @@ private async Task UpdateContentUrlAsync(IPublishableWorkbook workbook, XAttribu var sourceContentUrl = attr.Value; //Find the published data source reference. - var destDataSourceRef = await _mappedDataSourceFinder.FindDestinationReferenceAsync(sourceContentUrl, cancel) + var destDataSourceRef = await _mappedDataSourceFinder.FindBySourceContentUrlAsync(sourceContentUrl, cancel) .ConfigureAwait(false); if (destDataSourceRef is not null) @@ -196,7 +199,7 @@ protected override bool NeedsXmlTransforming(IPublishableWorkbook ctx) => ctx.Connections.Any(c => c.Type is TABLEAU_SERVER_CONNECTION_CLASS); /// - public override async Task ExecuteAsync(IPublishableWorkbook ctx, XDocument xml, CancellationToken cancel) + public override async Task TransformAsync(IPublishableWorkbook ctx, XDocument xml, CancellationToken cancel) { //Update elements. foreach (var connectionElement in xml.GetFeatureFlaggedDescendants(CONNECTION_EL)) diff --git a/src/Tableau.Migration/Engine/Hooks/Transformers/IXmlContentTransformer.cs b/src/Tableau.Migration/Engine/Hooks/Transformers/IXmlContentTransformer.cs index 23b9030..db8277c 100644 --- a/src/Tableau.Migration/Engine/Hooks/Transformers/IXmlContentTransformer.cs +++ b/src/Tableau.Migration/Engine/Hooks/Transformers/IXmlContentTransformer.cs @@ -47,7 +47,7 @@ public interface IXmlContentTransformer : IContentTransformer /// A cancellation token to obey. /// A task to await. - Task ExecuteAsync(TPublish ctx, XDocument xml, CancellationToken cancel); + Task TransformAsync(TPublish ctx, XDocument xml, CancellationToken cancel); /// async Task IMigrationHook.ExecuteAsync(TPublish ctx, CancellationToken cancel) @@ -56,13 +56,13 @@ public interface IXmlContentTransformer : IContentTransformer : IXmlContentTransform bool IXmlContentTransformer.NeedsXmlTransforming(TPublish ctx) => NeedsXmlTransforming(ctx); /// - public abstract Task ExecuteAsync(TPublish ctx, XDocument xml, CancellationToken cancel); + public abstract Task TransformAsync(TPublish ctx, XDocument xml, CancellationToken cancel); } } diff --git a/src/Tableau.Migration/Engine/IServiceCollectionExtensions.cs b/src/Tableau.Migration/Engine/IServiceCollectionExtensions.cs index 71096e5..e7a05bb 100644 --- a/src/Tableau.Migration/Engine/IServiceCollectionExtensions.cs +++ b/src/Tableau.Migration/Engine/IServiceCollectionExtensions.cs @@ -97,13 +97,13 @@ internal static IServiceCollection AddMigrationEngine(this IServiceCollection se //Caches/Content Finders //Register concrete types so that the easy way to get interface types is through IMigrationPipeline. services.AddScoped(typeof(BulkSourceCache<>)); - services.AddScoped(typeof(ManifestSourceContentReferenceFinder<>)); - services.AddScoped(); + services.AddScoped(typeof(ISourceContentReferenceFinder<>), typeof(ManifestSourceContentReferenceFinder<>)); + services.AddScoped(); services.AddScoped(typeof(BulkDestinationCache<>)); services.AddScoped(); - services.AddScoped(typeof(ManifestDestinationContentReferenceFinder<>)); - services.AddScoped(); + services.AddScoped(typeof(IDestinationContentReferenceFinder<>), typeof(ManifestDestinationContentReferenceFinder<>)); + services.AddScoped(); //Pipelines. services.AddScoped(); diff --git a/src/Tableau.Migration/Engine/MigrationInput.cs b/src/Tableau.Migration/Engine/MigrationInput.cs index 380e8c2..3440d87 100644 --- a/src/Tableau.Migration/Engine/MigrationInput.cs +++ b/src/Tableau.Migration/Engine/MigrationInput.cs @@ -47,7 +47,7 @@ public MigrationInput(ILogger log, ISharedResourcesLocalizer loc /// public IMigrationPlan Plan { - get => _plan ?? throw new InvalidOperationException($"{nameof(MigrationInput)} must be initialized before it it used."); + get => _plan ?? throw new InvalidOperationException($"{nameof(MigrationInput)} must be initialized before it is used."); private set => _plan = value; } private IMigrationPlan? _plan; diff --git a/src/Tableau.Migration/Engine/Pipelines/IMigrationPipeline.cs b/src/Tableau.Migration/Engine/Pipelines/IMigrationPipeline.cs index e3fe233..8be9dbf 100644 --- a/src/Tableau.Migration/Engine/Pipelines/IMigrationPipeline.cs +++ b/src/Tableau.Migration/Engine/Pipelines/IMigrationPipeline.cs @@ -77,14 +77,6 @@ IContentReferenceCache CreateSourceCache() IContentReferenceCache CreateDestinationCache() where TContent : class, IContentReference; - /// - /// Gets the destination content finder for the given content type. - /// - /// The content type. - /// The destination content finder. - IMappedContentReferenceFinder CreateDestinationFinder() - where TContent : class, IContentReference; - /// /// Gets the destination locked project cache. /// diff --git a/src/Tableau.Migration/Engine/Pipelines/MigrationPipelineBase.cs b/src/Tableau.Migration/Engine/Pipelines/MigrationPipelineBase.cs index d0cd9d0..035810f 100644 --- a/src/Tableau.Migration/Engine/Pipelines/MigrationPipelineBase.cs +++ b/src/Tableau.Migration/Engine/Pipelines/MigrationPipelineBase.cs @@ -127,13 +127,6 @@ public virtual IContentReferenceCache CreateDestinationCache() } } - /// - public virtual IMappedContentReferenceFinder CreateDestinationFinder() - where TContent : class, IContentReference - { - return Services.GetRequiredService>(); - } - /// public virtual ILockedProjectCache GetDestinationLockedProjectCache() => Services.GetRequiredService(); diff --git a/src/Tableau.Migration/Engine/Preparation/ContentItemPreparerBase.cs b/src/Tableau.Migration/Engine/Preparation/ContentItemPreparerBase.cs index fd12d75..56e6cad 100644 --- a/src/Tableau.Migration/Engine/Preparation/ContentItemPreparerBase.cs +++ b/src/Tableau.Migration/Engine/Preparation/ContentItemPreparerBase.cs @@ -20,8 +20,8 @@ using System.Threading.Tasks; using Tableau.Migration.Content; using Tableau.Migration.Content.Files; +using Tableau.Migration.Engine.Endpoints.Search; using Tableau.Migration.Engine.Hooks.Transformers; -using Tableau.Migration.Engine.Pipelines; namespace Tableau.Migration.Engine.Preparation { @@ -34,17 +34,19 @@ public abstract class ContentItemPreparerBase : IContentItem where TPublish : class { private readonly IContentTransformerRunner _transformerRunner; - private readonly IMigrationPipeline _pipeline; + private readonly IDestinationContentReferenceFinderFactory _destinationFinderFactory; /// /// Creates a new object. /// /// A transformer runner. - /// The migration pipeline. - public ContentItemPreparerBase(IContentTransformerRunner transformerRunner, IMigrationPipeline pipeline) + /// The destination finder factory. + public ContentItemPreparerBase( + IContentTransformerRunner transformerRunner, + IDestinationContentReferenceFinderFactory destinationFinderFactory) { _transformerRunner = transformerRunner; - _pipeline = pipeline; + _destinationFinderFactory = destinationFinderFactory; } /// @@ -103,15 +105,15 @@ protected virtual async Task ApplyMappingAsync(TPublish publishItem, ContentLoca else if(mappedParentLocation != containerContent.Container?.Location) { //If the mapping set a new parent, find based on the destination location. - var destinationFinder = _pipeline.CreateDestinationFinder(); - newParent = await destinationFinder.FindMappedDestinationReferenceAsync(mappedLocation.Parent(), cancel) + var destinationFinder = _destinationFinderFactory.ForDestinationContentType(); + newParent = await destinationFinder.FindByMappedLocationAsync(mappedLocation.Parent(), cancel) .ConfigureAwait(false); } else if(containerContent.Container is not null) { //If the mapping uses the same parent, find where that parent mapped to. - var destinationFinder = _pipeline.CreateDestinationFinder(); - newParent = await destinationFinder.FindDestinationReferenceAsync(containerContent.Container.Location, cancel) + var destinationFinder = _destinationFinderFactory.ForDestinationContentType(); + newParent = await destinationFinder.FindBySourceLocationAsync(containerContent.Container.Location, cancel) .ConfigureAwait(false); } else diff --git a/src/Tableau.Migration/Engine/Preparation/EndpointContentItemPreparer.cs b/src/Tableau.Migration/Engine/Preparation/EndpointContentItemPreparer.cs index 4e76807..b316c43 100644 --- a/src/Tableau.Migration/Engine/Preparation/EndpointContentItemPreparer.cs +++ b/src/Tableau.Migration/Engine/Preparation/EndpointContentItemPreparer.cs @@ -17,10 +17,9 @@ using System.Threading; using System.Threading.Tasks; -using Tableau.Migration.Content.Files; using Tableau.Migration.Engine.Endpoints; +using Tableau.Migration.Engine.Endpoints.Search; using Tableau.Migration.Engine.Hooks.Transformers; -using Tableau.Migration.Engine.Pipelines; namespace Tableau.Migration.Engine.Preparation { @@ -40,10 +39,12 @@ public class EndpointContentItemPreparer : ContentItemPrepar /// /// The source endpoint. /// - /// - public EndpointContentItemPreparer(ISourceEndpoint source, - IContentTransformerRunner transformerRunner, IMigrationPipeline pipeline) - : base(transformerRunner, pipeline) + /// + public EndpointContentItemPreparer( + ISourceEndpoint source, + IContentTransformerRunner transformerRunner, + IDestinationContentReferenceFinderFactory destinationFinderFactory) + : base(transformerRunner, destinationFinderFactory) { _source = source; } diff --git a/src/Tableau.Migration/Engine/Preparation/SourceContentItemPreparer.cs b/src/Tableau.Migration/Engine/Preparation/SourceContentItemPreparer.cs index 82d05cf..091ee2c 100644 --- a/src/Tableau.Migration/Engine/Preparation/SourceContentItemPreparer.cs +++ b/src/Tableau.Migration/Engine/Preparation/SourceContentItemPreparer.cs @@ -17,9 +17,8 @@ using System.Threading; using System.Threading.Tasks; -using Tableau.Migration.Content.Files; +using Tableau.Migration.Engine.Endpoints.Search; using Tableau.Migration.Engine.Hooks.Transformers; -using Tableau.Migration.Engine.Pipelines; namespace Tableau.Migration.Engine.Preparation { @@ -35,9 +34,11 @@ public class SourceContentItemPreparer : ContentItemPreparerBase. /// /// - /// - public SourceContentItemPreparer(IContentTransformerRunner transformerRunner, IMigrationPipeline pipeline) - : base(transformerRunner, pipeline) + /// + public SourceContentItemPreparer( + IContentTransformerRunner transformerRunner, + IDestinationContentReferenceFinderFactory destinationFinderFactory) + : base(transformerRunner, destinationFinderFactory) { } /// diff --git a/src/Tableau.Migration/Interop/Hooks/Transformers/ISyncXmlContentTransformer.cs b/src/Tableau.Migration/Interop/Hooks/Transformers/ISyncXmlContentTransformer.cs index 35b29de..715e61c 100644 --- a/src/Tableau.Migration/Interop/Hooks/Transformers/ISyncXmlContentTransformer.cs +++ b/src/Tableau.Migration/Interop/Hooks/Transformers/ISyncXmlContentTransformer.cs @@ -49,10 +49,10 @@ public interface ISyncXmlContentTransformer : IXmlContentTransformer - void Execute(TPublish ctx, XDocument xml); + void Transform(TPublish ctx, XDocument xml); /// - Task IXmlContentTransformer.ExecuteAsync(TPublish ctx, XDocument xml, CancellationToken cancel) - => Task.Run(() => Execute(ctx, xml), cancel); + Task IXmlContentTransformer.TransformAsync(TPublish ctx, XDocument xml, CancellationToken cancel) + => Task.Run(() => Transform(ctx, xml), cancel); } } diff --git a/src/Tableau.Migration/Interop/IServiceCollectionExtensions.cs b/src/Tableau.Migration/Interop/IServiceCollectionExtensions.cs index 5710944..48d71e6 100644 --- a/src/Tableau.Migration/Interop/IServiceCollectionExtensions.cs +++ b/src/Tableau.Migration/Interop/IServiceCollectionExtensions.cs @@ -34,16 +34,21 @@ public static class IServiceCollectionExtensions /// Add python support by adding python logging and configuration via environment variables. /// This will clear all existing s. All other logger provider should be added after this call. /// - /// - /// + /// The service collection to register services with. + /// A factory to use to create new loggers for a given category name. + /// The configuration options to initialize the SDK with. /// - public static IServiceCollection AddPythonSupport(this IServiceCollection services, Func pythonProviderFactory) + public static IServiceCollection AddPythonSupport( + this IServiceCollection services, + Func loggerFactory, + IConfiguration? userOptions = null) { services + .AddTableauMigrationSdk(userOptions) // Replace the default IUserAgentSuffixProvider with the python one .Replace(new ServiceDescriptor(typeof(IUserAgentSuffixProvider), typeof(PythonUserAgentSuffixProvider), ServiceLifetime.Singleton)) // Add Python Logging - .AddLogging(b => b.AddPythonLogging(pythonProviderFactory)) + .AddLogging(b => b.AddPythonLogging(loggerFactory)) // Add environment variable configuration .AddEnvironmentVariableConfiguration(); return services; @@ -53,13 +58,15 @@ public static IServiceCollection AddPythonSupport(this IServiceCollection servic /// Adds a python support, including supporting the python logger /// /// The - /// Function that creates a new + /// A factory to use to create new loggers for a given category name. /// - public static ILoggingBuilder AddPythonLogging(this ILoggingBuilder builder, Func pythonProviderFactory) + public static ILoggingBuilder AddPythonLogging( + this ILoggingBuilder builder, + Func loggerFactory) { // Clear all previous providers builder.ClearProviders(); - builder.Services.TryAddSingleton(pythonProviderFactory); + builder.Services.TryAddSingleton(new NonGenericLoggerProvider(loggerFactory)); // Enable all logs from .NET // They will be filtered on the migration_logger.py class builder.AddFilter(null, LogLevel.Trace); diff --git a/src/Tableau.Migration/Interop/InteropHelper.cs b/src/Tableau.Migration/Interop/InteropHelper.cs index a5f96e7..f517a1e 100644 --- a/src/Tableau.Migration/Interop/InteropHelper.cs +++ b/src/Tableau.Migration/Interop/InteropHelper.cs @@ -18,6 +18,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using Tableau.Migration.Content; namespace Tableau.Migration.Interop { @@ -26,93 +28,118 @@ namespace Tableau.Migration.Interop /// public static class InteropHelper { - internal static List ignoredMethods = new() + internal static HashSet _ignoredMethods = new() { - "GetHashCode", - "ToString", + nameof(object.GetHashCode), + nameof(object.ToString), + nameof(object.GetType), + nameof(object.Equals), + nameof(Enum.HasFlag), "Deconstruct", - "GetType", - "Equals", "GetTypeCode", "CompareTo", - "HasFlag" + "$" }; + private static Type MakeSampleGenericType(Type generic) + { + var genericTypes = generic.GetGenericArguments() + .Select(gt => typeof(IUser)) + .ToArray(); + + return generic.MakeGenericType(genericTypes); + } + + private static bool IsPropertyMethod(MethodInfo method) + => method.Name.StartsWith("get_") || method.Name.StartsWith("set_"); + + private static bool IsOperator(MethodInfo method) + => method.Name.StartsWith("op_"); + /// - /// Gets methods of a class that aren't "generic" + /// Gets the methods of a class. /// - /// The objects type to get methods from - /// List of method names - public static List GetMethods() - { - List methods; - List ret = new(); + /// The type to get methods from. + /// The method names. + public static IEnumerable GetMethods() + => GetMethods(typeof(T)); - if (typeof(T).IsInterface) - { - methods = typeof(T).GetAllInterfaceMethods().Select(p => p.Name).ToList(); - } - else + /// + /// Gets the methods of a class. + /// + /// The type to get methods from. + /// The method names. + public static IEnumerable GetMethods(Type type) + { + if(type.ContainsGenericParameters) { - methods = typeof(T).GetMethods().Select(p => p.Name).ToList(); + return GetMethods(MakeSampleGenericType(type)); } - foreach (var method in methods) - { - if (!method.StartsWith("get_") && // getters - !method.StartsWith("set_") && // setters - !method.StartsWith("op_") && // operators - !ignoredMethods.Contains(method) - ) - { - ret.Add(method); - } - } + var methods = type.IsInterface ? type.GetAllInterfaceMethods() : type.GetMethods(); - return ret; + return methods + .Where(m => !IsPropertyMethod(m)) + .Where(m => !IsOperator(m)) + .Where(m => !_ignoredMethods.Contains(m.Name)) + .Select(m => m.Name); } /// - /// Gets the properies of a class + /// Gets the properies of a class. /// - /// The objects type to get properties from - /// List of property names - public static List GetProperties() + /// The type to get properties from. + /// The property names. + public static IEnumerable GetProperties() + => GetProperties(typeof(T)); + + /// + /// Gets the properies of a class. + /// + /// The type to get properties from. + /// The property names. + public static IEnumerable GetProperties(Type type) { - if (typeof(T).IsInterface) - { - return typeof(T).GetAllInterfaceProperties().Select(p => p.Name).ToList(); - } - else + if (type.ContainsGenericParameters) { - return typeof(T).GetProperties().Select(p => p.Name).ToList(); + return GetProperties(MakeSampleGenericType(type)); } - } + var properties = type.IsInterface ? type.GetAllInterfaceProperties() : type.GetProperties(); + + return properties.Select(p => p.Name); + } /// - /// Gets all the names and values of a enum and returns them as a list. + /// Gets all the names and values of an enumeration. /// - /// The enum type - /// List of tuples with all the names and values - static public List> GetEnum() where T : Enum + /// The enum type. + /// Tuples with all the names and values. + public static IEnumerable> GetEnum() { - var ret = new List>(); - var enumType = typeof(T); - var underlyingType = Enum.GetUnderlyingType(enumType); + if (enumType.BaseType == typeof(Enum)) + { + var underlyingType = Enum.GetUnderlyingType(enumType); - var names = Enum.GetNames(enumType); + foreach (var name in Enum.GetNames(enumType)) + { + var value = Enum.Parse(enumType, name); + object underlyingValue = Convert.ChangeType(value, underlyingType); - foreach (var name in names) + yield return new(name, underlyingValue); + } + } + else if(enumType.BaseType == typeof(StringEnum)) { - var value = Enum.Parse(enumType, name); - object underlyingValue = Convert.ChangeType(value, underlyingType); + foreach(var field in enumType.GetFields(BindingFlags.Public | BindingFlags.Static)) + { + if (field is null || !field.IsLiteral || field.IsInitOnly) + continue; - ret.Add(new Tuple(name, underlyingValue)); + yield return new(field.Name, field.GetValue(null)!); + } } - - return ret; } } } diff --git a/src/Tableau.Migration/Interop/Logging/NonGenericLoggerBase.cs b/src/Tableau.Migration/Interop/Logging/NonGenericLoggerBase.cs index 6b8b944..183d2bc 100644 --- a/src/Tableau.Migration/Interop/Logging/NonGenericLoggerBase.cs +++ b/src/Tableau.Migration/Interop/Logging/NonGenericLoggerBase.cs @@ -47,7 +47,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { // In python.net, a generic method like Log can't be concretely implemented because python has no understanding of generic methods. // To get around this, we pre-format the message, and pass it to the non-generic `Log` method - this.Log(logLevel, eventId, formatter(state, null), exception, formatter(state, exception)); + Log(logLevel, eventId, formatter(state, null), exception, formatter(state, exception)); } /// diff --git a/src/Tableau.Migration/Interop/Logging/NonGenericLoggerProvider.cs b/src/Tableau.Migration/Interop/Logging/NonGenericLoggerProvider.cs index 1a0d16f..6acb588 100644 --- a/src/Tableau.Migration/Interop/Logging/NonGenericLoggerProvider.cs +++ b/src/Tableau.Migration/Interop/Logging/NonGenericLoggerProvider.cs @@ -27,7 +27,7 @@ namespace Tableau.Migration.Interop.Logging /// [UnsupportedOSPlatform("browser")] [ProviderAlias("NonGeneric")] - public class NonGenericLoggerProvider : ILoggerProvider + internal class NonGenericLoggerProvider : ILoggerProvider { private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase); diff --git a/src/Tableau.Migration/Tableau.Migration.csproj b/src/Tableau.Migration/Tableau.Migration.csproj index 6e9fcbf..34d03f2 100644 --- a/src/Tableau.Migration/Tableau.Migration.csproj +++ b/src/Tableau.Migration/Tableau.Migration.csproj @@ -15,7 +15,6 @@ false NUGET.md Tableau;migration;API;REST;workbook;datasource;project;group;user;permission - MIT True TableauLogoColor.png Customers worldwide are migrating to Tableau Cloud to take advantage of the innovation, reduced cost, security, and scalability of Tableau’s managed data service. The Migration Software Development Kit (SDK) helps you migrate to Tableau Cloud in a seamless and predictable way. You can use the Migration SDK to build your own migration application to do the following: diff --git a/src/Tableau.Migration/TaskExtensions.cs b/src/Tableau.Migration/TaskExtensions.cs index c1a9010..e2efc11 100644 --- a/src/Tableau.Migration/TaskExtensions.cs +++ b/src/Tableau.Migration/TaskExtensions.cs @@ -22,7 +22,7 @@ namespace Tableau.Migration /// /// Static class containing extension methods for and objects. /// - internal static class TaskExtensions + public static class TaskExtensions { /// /// Get the results synchronously, applying best practices. diff --git a/tests/Python.TestApplication/Python.TestApplication.pyproj b/tests/Python.TestApplication/Python.TestApplication.pyproj index b795ab9..2e82ee2 100644 --- a/tests/Python.TestApplication/Python.TestApplication.pyproj +++ b/tests/Python.TestApplication/Python.TestApplication.pyproj @@ -28,12 +28,12 @@ + - diff --git a/tests/Python.TestApplication/config.json b/tests/Python.TestApplication/config.json index 905dc30..99ec228 100644 --- a/tests/Python.TestApplication/config.json +++ b/tests/Python.TestApplication/config.json @@ -22,7 +22,9 @@ "special_users": { "admin_domain": "", "admin_username": "", - "emails": [ ] + "emails": [] }, + "skipped_project": "", + "skipped_parent_destination": "Missing Parent", "previous_manifest": "" } diff --git a/tests/Python.TestApplication/main.py b/tests/Python.TestApplication/main.py index 5a99b16..6f9811b 100644 --- a/tests/Python.TestApplication/main.py +++ b/tests/Python.TestApplication/main.py @@ -30,36 +30,63 @@ from threading import Thread # tableau_migration and migration_testcomponents -from tableau_migration.migration import PyMigrationResult, PyMigrationManifest -from tableau_migration.migration_engine import PyMigrationPlanBuilder -from tableau_migration.migration_engine_migrators import PyMigrator -from migration_testcomponents_engine_manifest import PyMigrationManifestSerializer -from migration_testcomponents_filters import PySpecialUserFilter, PyUnlicensedUserFilter +from tableau_migration import ( + IMigrationManifestEntry, + Migrator, + MigrationPlanBuilder, + MigrationManifestEntryStatus, + cancellation_token_source +) +from tableau_migration.migration import ( + PyMigrationResult, + PyMigrationManifest +) +from migration_testcomponents_engine_manifest import ( + PyMigrationManifestSerializer +) +from migration_testcomponents_filters import ( + # Skip Filters: Uncomment when neccesary. + # SkipAllUsersFilter, + # SkipAllGroupsFilter, + # SkipAllProjectsFilter, + # SkipAllDataSourcesFilter, + # SkipAllWorkbooksFilter, + # Skip Filters: Uncomment when neccesary. + SkipProjectByParentLocationFilter, + SkipDataSourceByParentLocationFilter, + SkipWorkbookByParentLocationFilter, + SpecialUserFilter, + UnlicensedUserFilter +) from migration_testcomponents_hooks import ( - PySaveManifestAfterBatch_User, - PySaveManifestAfterBatch_Group, - PySaveManifestAfterBatch_Project, - PySaveManifestAfterBatch_DataSource, - PySaveManifestAfterBatch_Workbook, - PyTimeLoggerAfterActionHook, - save_manifest_after_batch_user_factory, - save_manifest_after_batch_group_factory, - save_manifest_after_batch_project_factory, - save_manifest_after_batch_datasource_factory, - save_manifest_after_batch_workbook_factory) -from migration_testcomponents_mappings import PySpecialUserMapping, PyTestTableauCloudUsernameMapping, PyUnlicensedUserMapping + SaveUserManifestHook, + SaveGroupManifestHook, + SaveProjectManifestHook, + SaveDataSourceManifestHook, + SaveWorkbookManifestHook, + TimeLoggerAfterActionHook +) +from migration_testcomponents_mappings import ( + SpecialUserMapping, + TestTableauCloudUsernameMapping, + ProjectWithinSkippedLocationMapping, + DataSourceWithinSkippedLocationMapping, + WorkbookWithinSkippedLocationMapping, + UnlicensedUserMapping +) +from migration_testcomponents_transformers import ( + RemoveMissingDestinationUsersFromGroupsTransformer +) # CSharp imports -from System import Func, IServiceProvider - -from Tableau.Migration.Content import IUser from Tableau.Migration.Engine.Pipelines import ServerToCloudMigrationPipeline -from Tableau.Migration.Engine.Manifest import MigrationManifestEntryStatus from Tableau.Migration.TestComponents import IServiceCollectionExtensions as MigrationTestComponentsSCE class Program(): """Main program class.""" + + done = False def __init__(self): """Program init, sets up logging.""" @@ -98,9 +125,10 @@ def print_result(self, result: PyMigrationResult): for pipeline_content_type in ServerToCloudMigrationPipeline.ContentTypes: content_type = pipeline_content_type.ContentType - type_result = result.manifest.entries.ForContentType(content_type) + result.manifest.entries + type_entries = [IMigrationManifestEntry(x) for x in result.manifest.entries.ForContentType(content_type)] - count_total = type_result.Count + count_total = len(type_entries) count_migrated = 0 count_skipped = 0 @@ -108,16 +136,16 @@ def print_result(self, result: PyMigrationResult): count_cancelled = 0 count_pending = 0 - for item in type_result: - if item.Status == MigrationManifestEntryStatus.Migrated: + for entry in type_entries: + if entry.status == MigrationManifestEntryStatus.MIGRATED: count_migrated += 1 - elif item.Status == MigrationManifestEntryStatus.Skipped: + elif entry.status == MigrationManifestEntryStatus.SKIPPED: count_skipped += 1 - elif item.Status == MigrationManifestEntryStatus.Error: + elif entry.status == MigrationManifestEntryStatus.ERROR: count_errored += 1 - elif item.Status == MigrationManifestEntryStatus.Canceled: + elif entry.status == MigrationManifestEntryStatus.CANCELED: count_cancelled += 1 - elif item.Status == MigrationManifestEntryStatus.Pending: + elif entry.status == MigrationManifestEntryStatus.PENDING: count_pending += 1 output = f''' @@ -134,15 +162,15 @@ def print_result(self, result: PyMigrationResult): def migrate(self): """The main migration function.""" self.logger.info("Starting migration") + # Add the C# test components we've ported to Python and register them with the DI Service Provider MigrationTestComponentsSCE.AddTestComponents(tableau_migration._service_collection) - tableau_migration.migration._build_service_provider() + tableau_migration.migration._build_service_provider(tableau_migration._service_collection) # Setup base objects for migrations - plan_builder = PyMigrationPlanBuilder() - migration = PyMigrator() - self._manifest_serializer = PyMigrationManifestSerializer() + plan_builder = MigrationPlanBuilder() + migration = Migrator() # Build the plan plan_builder = plan_builder \ @@ -158,37 +186,42 @@ def migrate(self): access_token = os.environ.get('TABLEAU_MIGRATION_DESTINATION_TOKEN', helper.config['destination']['access_token'])) \ .for_server_to_cloud() \ .with_tableau_id_authentication_type() \ - .with_tableau_cloud_usernames(PyTestTableauCloudUsernameMapping()) + .with_tableau_cloud_usernames(TestTableauCloudUsernameMapping) self.logger.info("Adding Special User filter and mapping") - plan_builder.filters.add(IUser, PySpecialUserFilter()) - plan_builder.mappings.add(IUser, PySpecialUserMapping()) + plan_builder.filters.add(SpecialUserFilter) + plan_builder.mappings.add(SpecialUserMapping) self.logger.info("Adding unlicensed user filter and mapping") - plan_builder.filters.add(IUser, PyUnlicensedUserFilter()) - plan_builder.mappings.add(IUser, PyUnlicensedUserMapping()) + plan_builder.filters.add(UnlicensedUserFilter) + plan_builder.mappings.add(UnlicensedUserMapping) self.logger.info("Adding Hooks") - plan_builder.hooks.add(PyTimeLoggerAfterActionHook(self.consoleHandler)) + TimeLoggerAfterActionHook.handler = self.consoleHandler + plan_builder.hooks.add(TimeLoggerAfterActionHook) - # This adds the PySaveManifestAfterBatch_ hook via the save_manifest_after_batch__factory function. - # This is required because the manifest comes from the IMigration, which is scoped. - # The global DI provider can not create the scoped IMigration. - # the save_manifest_after_batch__factory function takes the IServiceProvider, which is scoped when save_manifest_after_batch__factory is called - # This means save_manifest_after_batch__factory can create the PySaveManifestAfterBatch_ and pass in the IMigration - plan_builder.hooks.add(PySaveManifestAfterBatch_User, Func[IServiceProvider, PySaveManifestAfterBatch_User](save_manifest_after_batch_user_factory)) - plan_builder.hooks.add(PySaveManifestAfterBatch_Group, Func[IServiceProvider, PySaveManifestAfterBatch_Group](save_manifest_after_batch_group_factory)) - plan_builder.hooks.add(PySaveManifestAfterBatch_Project, Func[IServiceProvider, PySaveManifestAfterBatch_Project](save_manifest_after_batch_project_factory)) - plan_builder.hooks.add(PySaveManifestAfterBatch_DataSource, Func[IServiceProvider, PySaveManifestAfterBatch_DataSource](save_manifest_after_batch_datasource_factory)) - plan_builder.hooks.add(PySaveManifestAfterBatch_Workbook, Func[IServiceProvider, PySaveManifestAfterBatch_Workbook](save_manifest_after_batch_workbook_factory)) + plan_builder.hooks.add(SaveUserManifestHook) + plan_builder.hooks.add(SaveGroupManifestHook) + plan_builder.hooks.add(SaveProjectManifestHook) + plan_builder.hooks.add(SaveDataSourceManifestHook) + plan_builder.hooks.add(SaveWorkbookManifestHook) + + plan_builder.filters.add(SkipProjectByParentLocationFilter) + plan_builder.filters.add(SkipDataSourceByParentLocationFilter) + plan_builder.filters.add(SkipWorkbookByParentLocationFilter) + plan_builder.transformers.add(RemoveMissingDestinationUsersFromGroupsTransformer) + plan_builder.mappings.add(ProjectWithinSkippedLocationMapping) + plan_builder.mappings.add(DataSourceWithinSkippedLocationMapping) + plan_builder.mappings.add(WorkbookWithinSkippedLocationMapping) - # Comment out an neccesary - #planBuilder.filters.add(IUser, PySkipFilter_Users()) - #planBuilder.filters.add(IGroup, PySkipFilter_Groups()) - #planBuilder.filters.add(IProject, PySkipFilter_Projects()) - #planBuilder.filters.add(IDataSource, PySkipFilter_DataSources()) - #planBuilder.filters.add(IWorkbook, PySkipFilter_Workbooks()) + # Skip Filters: Uncomment when neccesary. + # plan_builder.filters.add(SkipAllUsersFilter) + # plan_builder.filters.add(SkipAllGroupsFilter) + # plan_builder.filters.add(SkipAllProjectsFilter) + # plan_builder.filters.add(SkipAllDataSourcesFilter) + # plan_builder.filters.add(SkipAllWorkbooksFilter) + # Skip Filters: Uncomment when neccesary. # Load manifest if available prev_manifest = self.load_manifest(helper.config['previous_manifest']) @@ -210,7 +243,9 @@ def migrate(self): self.logger.info(f'Migration Ended: {time.ctime(end_time)}') self.logger.info(f'Elapsed: {end_time - start_time}') - print("All done") + print("All done") + + self.done = True if __name__ == '__main__': @@ -218,14 +253,13 @@ def migrate(self): program = Program() thread = Thread(target = program.migrate) thread.start() - done = False - while not done: + while not program.done: try: thread.join(1) except KeyboardInterrupt: print("Caught Ctrl+C, shutting down...") - tableau_migration.cancellation_token_source.Cancel() + cancellation_token_source.Cancel() thread.join() - done = True + program.done = True diff --git a/tests/Python.TestApplication/migration_testcomponents_engine_manifest.py b/tests/Python.TestApplication/migration_testcomponents_engine_manifest.py index 80c1f59..18de95f 100644 --- a/tests/Python.TestApplication/migration_testcomponents_engine_manifest.py +++ b/tests/Python.TestApplication/migration_testcomponents_engine_manifest.py @@ -14,7 +14,7 @@ # limitations under the License. """Wrapper for classes in Tableau.Migration.TestComponents.Engine.Manifest namespace.""" -import tableau_migration.migration +import tableau_migration from Tableau.Migration.TestComponents.Engine.Manifest import MigrationManifestSerializer from System.Text.Json import JsonSerializerOptions @@ -30,36 +30,46 @@ def __init__(self) -> None: self._services = tableau_migration.migration.get_service_provider() self._migration_manifest_serializer = tableau_migration.migration.get_service(self._services, MigrationManifestSerializer) - def save(self, manifest: PyMigrationManifest, path: str, cancel = tableau_migration.cancellation_token, json_options: JsonSerializerOptions = None) -> None: + def save(self, manifest: PyMigrationManifest, path: str, cancel = None, json_options: JsonSerializerOptions = None) -> None: """ Saves a manifest in JSON format. Args: manifest (PyMigrationManifest): The manifest to save. path (str): The file path to save the manifest to. - cancel (_type_, optional): A cancellation token to obey. Defaults to tableau_migration.cancellation_token. + cancel (_type_, optional): A cancellation token to obey. json_options (JsonSerializerOptions, optional): Optional JSON options to use. Defaults to None. Returns: None """ + + # Defaults to global singleton cancellation token. + if cancel is None: + cancel = tableau_migration.cancellation_token + if(json_options is None): return self._migration_manifest_serializer.SaveAsync(manifest._migration_manifest, path).GetAwaiter().GetResult() else: return self._migration_manifest_serializer.SaveAsync(manifest._migration_manifest, path, json_options).GetAwaiter().GetResult() - def load(self, path: str, cancel = tableau_migration.cancellation_token, json_options: JsonSerializerOptions = None) -> PyMigrationManifest | None: + def load(self, path: str, cancel = None, json_options: JsonSerializerOptions = None) -> PyMigrationManifest | None: """ Loads a manifest from JSON format. Args: path (str): The file path to load the manifest from. - cancel (_type_, optional): A cancellation token to obey. Defaults to tableau_migration.cancellation_token. + cancel (_type_, optional): A cancellation token to obey. json_options (JsonSerializerOptions, optional): Optional JSON options to use. Defaults to None. Returns: PyMigrationManifest: The PyMigrationManifest, or null if the manifest could not be loaded. """ + + # Defaults to global singleton cancellation token. + if cancel is None: + cancel = tableau_migration.cancellation_token + manifest = self._migration_manifest_serializer.LoadAsync(path,cancel,json_options).GetAwaiter().GetResult() if manifest is not None: return PyMigrationManifest(manifest) diff --git a/tests/Python.TestApplication/migration_testcomponents_filters.py b/tests/Python.TestApplication/migration_testcomponents_filters.py index c62eeb8..2084220 100644 --- a/tests/Python.TestApplication/migration_testcomponents_filters.py +++ b/tests/Python.TestApplication/migration_testcomponents_filters.py @@ -18,110 +18,159 @@ import helper import logging -from Tableau.Migration.Api.Rest.Models import LicenseLevels -from Tableau.Migration.Content import IUser, IGroup, IProject, IDataSource, IWorkbook -from Tableau.Migration.Interop.Hooks.Filters import ISyncContentFilter -from Tableau.Migration.Engine.Hooks.Filters import ContentFilterBase - - -class PySpecialUserFilter(ContentFilterBase[IUser]): +from typing import ( + Generic, + TypeVar +) +from tableau_migration import ( + ContentFilterBase, + ContentMigrationItem, + IDataSource, + IGroup, + IProject, + IUser, + IWorkbook, + LicenseLevels +) + +class SpecialUserFilter(ContentFilterBase[IUser]): """A class to filter special users.""" - - __namespace__ = "Python.TestApplication" - _dotnet_base = ContentFilterBase[IUser] def __init__(self): """Default init to set up logging.""" self._logger = logging.getLogger(self.__class__.__name__) self._logger.setLevel(logging.DEBUG) - def ShouldMigrate(self,item): # noqa: N802 - """Implements ShouldMigrate from base.""" + def should_migrate(self, item: ContentMigrationItem[IUser]) -> bool: # Need to improve this to be an array not a single item - if item.SourceItem.Email in helper.config['special_users']['emails']: - self._logger.debug('%s filtered %s', self.__class__.__name__, item.SourceItem.Email) + if item.source_item.email in helper.config['special_users']['emails']: + self._logger.debug('%s filtered %s', self.__class__.__name__, item.source_item.email) return False - return True - + return True -class PyUnlicensedUserFilter(ContentFilterBase[IUser]): +class UnlicensedUserFilter(ContentFilterBase[IUser]): """A class to filter unlicensed users.""" - - __namespace__ = "Python.TestApplication" - _dotnet_base = ContentFilterBase[IUser] def __init__(self): """Default init to set up logging.""" self._logger = logging.getLogger(self.__class__.__name__) self._logger.setLevel(logging.DEBUG) - def ShouldMigrate(self,item): # noqa: N802 - """Implements ShouldMigrate from base.""" - if item.SourceItem.LicenseLevel == LicenseLevels.Unlicensed: - self._logger.debug('%s filtered %s',self.__class__.__name__, item.SourceItem.Email) + def should_migrate(self, item: ContentMigrationItem[IUser]) -> bool: + if item.source_item.license_level == LicenseLevels.UNLICENSED: + self._logger.debug('%s filtered %s',self.__class__.__name__, item.source_item.email) return False return True - - -class PySkipFilter_Users(ContentFilterBase[IUser]): # noqa: N801 +class SkipAllUsersFilter(ContentFilterBase[IUser]): # noqa: N801 """A class to filter all users.""" - __namespace__ = "Python.TestApplication" - _dotnet_base = ContentFilterBase[IUser] - def __init__(self): """Default init to set up logging.""" self.logger = logging.getLogger(self.__class__.__name__) self.logger.setLevel(logging.DEBUG) - def ShouldMigrate(self,item): # noqa: N802 - """Implements ShouldMigrate from base.""" - self.logger.debug('%s is filtering %s', self.__class__.__name__, item.SourceItem.Email) + def should_migrate(self, item: ContentMigrationItem[IUser]) -> bool: + self.logger.debug('%s is filtering "%s"', self.__class__.__name__, item.source_item.name) return False - - -class PySkipFilter_Groups(ContentFilterBase[IGroup]): # noqa: N801 + +class SkipAllGroupsFilter(ContentFilterBase[IGroup]): # noqa: N801 """A class to filter all groups.""" - __namespace__ = "Python.TestApplication" - _dotnet_base = ContentFilterBase[IGroup] - - def ShouldMigrate(self,item): # noqa: N802 - """Implements ShouldMigrate from base.""" - return False - + def __init__(self): + """Default init to set up logging.""" + self.logger = logging.getLogger(self.__class__.__name__) + self.logger.setLevel(logging.DEBUG) -class PySkipFilter_Projects(ContentFilterBase[IProject]): # noqa: N801 + def should_migrate(self, item: ContentMigrationItem[IGroup]) -> bool: + self.logger.debug('%s is filtering "%s"', self.__class__.__name__, item.source_item.name) + return False + +class SkipAllProjectsFilter(ContentFilterBase[IProject]): # noqa: N801 """A class to filter all projects.""" - __namespace__ = "Python.TestApplication" - _dotnet_base = ContentFilterBase[IProject] - - def ShouldMigrate(self,item): # noqa: N802 - """Implements ShouldMigrate from base.""" - return False - + def __init__(self): + """Default init to set up logging.""" + self.logger = logging.getLogger(self.__class__.__name__) + self.logger.setLevel(logging.DEBUG) -class PySkipFilter_DataSources(ContentFilterBase[IDataSource]): # noqa: N801 + def should_migrate(self, item: ContentMigrationItem[IProject]) -> bool: + self.logger.debug('%s is filtering "%s"', self.__class__.__name__, item.source_item.name) + return False + +class SkipAllDataSourcesFilter(ContentFilterBase[IDataSource]): # noqa: N801 """A class to filter all data sources.""" - __namespace__ = "Python.TestApplication" - _dotnet_base = ContentFilterBase[IDataSource] - - def ShouldMigrate(self,item): # noqa: N802 - """Implements ShouldMigrate from base.""" + def __init__(self): + """Default init to set up logging.""" + self.logger = logging.getLogger(self.__class__.__name__) + self.logger.setLevel(logging.DEBUG) + + def should_migrate(self, item: ContentMigrationItem[IDataSource]) -> bool: + self.logger.debug('%s is filtering "%s"', self.__class__.__name__, item.source_item.name) return False - -class PySkipFilter_Workbooks(ContentFilterBase[IWorkbook]): # noqa: N801 +class SkipAllWorkbooksFilter(ContentFilterBase[IWorkbook]): # noqa: N801 """A class to filter all workbooks.""" - __namespace__ = "Python.TestApplication" - _dotnet_base = ContentFilterBase[IWorkbook] + def __init__(self): + """Default init to set up logging.""" + self.logger = logging.getLogger(self.__class__.__name__) + self.logger.setLevel(logging.DEBUG) + + def should_migrate(self, item: ContentMigrationItem[IWorkbook]) -> bool: + self.logger.debug('%s is filtering "%s"', self.__class__.__name__, item.source_item.name) + return False + +TContent = TypeVar("TContent") + +class _SkipContentByParentLocationFilter(Generic[TContent]): # noqa: N801 + """Generic base filter wrapper to filter content by parent location.""" - def ShouldMigrate(self,item): # noqa: N802 - """Implements ShouldMigrate from base.""" + def __init__(self, logger_name: str): + """Default init to set up logging.""" + self._logger = logging.getLogger(logger_name) + self._logger.setLevel(logging.DEBUG) + + def should_migrate(self, item: ContentMigrationItem[TContent], services) -> bool: + if item.source_item.location.parent().path != helper.config['skipped_project']: + return True + + source_project_finder = services.get_source_finder(IProject) + + content_reference = source_project_finder.find_by_source_location(item.source_item.location.parent()) + + self._logger.info('Skipping %s that belongs to "%s" (Project ID: %s)', self.__orig_class__.__args__[0].__name__, helper.config['skipped_project'], content_reference.id) return False + +class SkipProjectByParentLocationFilter(ContentFilterBase[IProject]): # noqa: N801 + """A class to filter projects from a given parent location.""" + + def __init__(self): + """Default init to set up wrapper.""" + self._filter = _SkipContentByParentLocationFilter[IProject](self.__class__.__name__) + + def should_migrate(self, item: ContentMigrationItem[IProject]) -> bool: + return self._filter.should_migrate(item, self.services) + +class SkipDataSourceByParentLocationFilter(ContentFilterBase[IDataSource]): # noqa: N801 + """A class to filter data sources from a given parent location.""" + + def __init__(self): + """Default init to set up wrapper.""" + self._filter = _SkipContentByParentLocationFilter[IDataSource](self.__class__.__name__) + + def should_migrate(self, item: ContentMigrationItem[IDataSource]) -> bool: + return self._filter.should_migrate(item, self.services) + +class SkipWorkbookByParentLocationFilter(ContentFilterBase[IWorkbook]): # noqa: N801 + """A class to filter workbooks from a given parent location.""" + + def __init__(self): + """Default init to set up wrapper.""" + self._filter = _SkipContentByParentLocationFilter[IWorkbook](self.__class__.__name__) + + def should_migrate(self, item: ContentMigrationItem[IWorkbook]) -> bool: + return self._filter.should_migrate(item, self.services) \ No newline at end of file diff --git a/tests/Python.TestApplication/migration_testcomponents_hooks.py b/tests/Python.TestApplication/migration_testcomponents_hooks.py index 8118003..a7ca2a1 100644 --- a/tests/Python.TestApplication/migration_testcomponents_hooks.py +++ b/tests/Python.TestApplication/migration_testcomponents_hooks.py @@ -15,179 +15,79 @@ """Hooks for the Python.TestApplication.""" -import logging -import helper +import logging +import helper +from typing import TypeVar import tableau_migration import tableau_migration.migration -from migration_testcomponents_engine_manifest import PyMigrationManifestSerializer -from Tableau.Migration.Content import IUser, IGroup, IProject, IDataSource, IWorkbook -from Tableau.Migration.Engine import IMigration -from Tableau.Migration.Engine.Actions import IMigrationActionResult -from Tableau.Migration.Interop.Hooks import ISyncContentBatchMigrationCompletedHook, ISyncMigrationActionCompletedHook +from tableau_migration import ( + ContentBatchMigrationCompletedHookBase, + IContentBatchMigrationResult, + MigrationActionCompletedHookBase, + IMigrationActionResult, + IDataSource, + IGroup, + IProject, + IUser, + IWorkbook +) -from System import IServiceProvider +from migration_testcomponents_engine_manifest import PyMigrationManifestSerializer -# This gets called every time the hook is called, meaning this object -# is created every single time -# This is less then ideal -def save_manifest_after_batch_user_factory(services: IServiceProvider): - """Factory function to build PySaveManifestAfterBatch_User.""" - migration = services.GetService(IMigration) - ret = PySaveManifestAfterBatch_User(PyMigrationManifestSerializer(), migration) - return ret +TContent = TypeVar("TContent") -def save_manifest_after_batch_group_factory(services: IServiceProvider): - """Factory function to build PySaveManifestAfterBatch_Group.""" - migration = services.GetService(IMigration) - ret = PySaveManifestAfterBatch_Group(PyMigrationManifestSerializer(), migration) - return ret - -def save_manifest_after_batch_project_factory(services: IServiceProvider): - """Factory function to build PySaveManifestAfterBatch_Project.""" - migration = services.GetService(IMigration) - ret = PySaveManifestAfterBatch_Project(PyMigrationManifestSerializer(), migration) - return ret - -def save_manifest_after_batch_datasource_factory(services: IServiceProvider): - """Factory function to build PySaveManifestAfterBatch_DataSource.""" - migration = services.GetService(IMigration) - ret = PySaveManifestAfterBatch_DataSource(PyMigrationManifestSerializer(), migration) - return ret - -def save_manifest_after_batch_workbook_factory(services: IServiceProvider): - """Factory function to build PySaveManifestAfterBatch_Workbook.""" - migration = services.GetService(IMigration) - ret = PySaveManifestAfterBatch_Workbook(PyMigrationManifestSerializer(), migration) - return ret - - -class PyTimeLoggerAfterActionHook(ISyncMigrationActionCompletedHook): +class TimeLoggerAfterActionHook(MigrationActionCompletedHookBase): """Logs the time when an action is complete.""" + + handler = None - __namespace__ = "Python.TestApplication" - _dotnet_base = ISyncMigrationActionCompletedHook - - def __init__(self, handler: logging.Handler | None = None): + def __init__(self): """Default init to set up logging.""" self._logger = logging.getLogger(self.__class__.__name__) self._logger.setLevel(logging.DEBUG) - if handler is not None: - self._logger.addHandler(handler) + if TimeLoggerAfterActionHook.handler is not None: + self._logger.addHandler(TimeLoggerAfterActionHook.handler) - def Execute(self, ctx : IMigrationActionResult): # noqa: N802 - """Implements Execute from base.""" - self._logger.info("Migration action completed") - return ctx - + def execute(self, ctx: IMigrationActionResult) -> IMigrationActionResult: # noqa: N802 + """Executes the hook.""" + self._logger.info("Migration action completed.") + return ctx -class PySaveManifestAfterBatch_User(ISyncContentBatchMigrationCompletedHook[IUser]): - """A update the manifest file after a user batch is migrated.""" +class SaveManifestHookBase(ContentBatchMigrationCompletedHookBase[TContent]): + """Updates the manifest file after a batch is migrated.""" - __namespace__ = "Python.TestApplication" - _dotnet_base = ISyncContentBatchMigrationCompletedHook[IUser] - - - def __init__(self, manifest_serializer: PyMigrationManifestSerializer, migration: IMigration) -> None: - """Default __init__.""" - self._manifest_serializer = manifest_serializer - self._migration = migration - + def __init__(self) -> None: + """Default __init__.""" self._logger = logging.getLogger(self.__class__.__name__) self._logger.setLevel(logging.DEBUG) - - def Execute(self, ctx): # noqa: N802 - """Implements Execute from base.""" - self._logger.debug("Saving manifest") - self._manifest_serializer.save(tableau_migration.migration.PyMigrationManifest(self._migration.Manifest), helper.manifest_path) - - -class PySaveManifestAfterBatch_Group(ISyncContentBatchMigrationCompletedHook[IGroup]): - """A update the manifest file after group batch is migrated.""" - - __namespace__ = "Python.TestApplication" - _dotnet_base = ISyncContentBatchMigrationCompletedHook[IGroup] - - - def __init__(self, manifest_serializer: PyMigrationManifestSerializer, migration: IMigration) -> None: - """Default __init__.""" - self._manifest_serializer = manifest_serializer - self._migration = migration + def execute(self, ctx: IContentBatchMigrationResult[TContent]) -> IContentBatchMigrationResult[TContent]: # noqa: N802 + """Executes the hook.""" + self._logger.debug("Saving manifest.") - self._logger = logging.getLogger(self.__class__.__name__) - self._logger.setLevel(logging.DEBUG) - - - def Execute(self, ctx): # noqa: N802 - """Implements Execute from base.""" - self._logger.debug("Saving manifest") - self._manifest_serializer.save(tableau_migration.migration.PyMigrationManifest(self._migration.Manifest), helper.manifest_path) - - -class PySaveManifestAfterBatch_Project(ISyncContentBatchMigrationCompletedHook[IProject]): - """A update the manifest file after project batch is migrated.""" - - __namespace__ = "Python.TestApplication" - _dotnet_base = ISyncContentBatchMigrationCompletedHook[IProject] + serializer = PyMigrationManifestSerializer() + manifest = self.services.get_manifest() + serializer.save(manifest, helper.manifest_path) - - def __init__(self, manifest_serializer: PyMigrationManifestSerializer, migration: IMigration) -> None: - """Default __init__.""" - self._manifest_serializer = manifest_serializer - self._migration = migration - - self._logger = logging.getLogger(self.__class__.__name__) - self._logger.setLevel(logging.DEBUG) - - - def Execute(self, ctx): # noqa: N802 - """Implements Execute from base.""" - self._logger.debug("Saving manifest") - self._manifest_serializer.save(tableau_migration.migration.PyMigrationManifest(self._migration.Manifest), helper.manifest_path) - - -class PySaveManifestAfterBatch_DataSource(ISyncContentBatchMigrationCompletedHook[IDataSource]): - """A update the manifest file after Data Source batch type is migrated.""" - - __namespace__ = "Python.TestApplication" - _dotnet_base = ISyncContentBatchMigrationCompletedHook[IDataSource] - - - def __init__(self, manifest_serializer: PyMigrationManifestSerializer, migration: IMigration) -> None: - """Default __init__.""" - self._manifest_serializer = manifest_serializer - self._migration = migration - - self._logger = logging.getLogger(self.__class__.__name__) - self._logger.setLevel(logging.DEBUG) - +class SaveUserManifestHook(SaveManifestHookBase[IUser]): + """Updates the manifest file after a user batch is migrated.""" + pass - def Execute(self, ctx): # noqa: N802 - """Implements Execute from base.""" - self._logger.debug("Saving manifest") - self._manifest_serializer.save(tableau_migration.migration.PyMigrationManifest(self._migration.Manifest), helper.manifest_path) - +class SaveGroupManifestHook(SaveManifestHookBase[IGroup]): + """Updates the manifest file after a group batch is migrated.""" + pass -class PySaveManifestAfterBatch_Workbook(ISyncContentBatchMigrationCompletedHook[IWorkbook]): - """A update the manifest file after a content type is migrated.""" +class SaveProjectManifestHook(SaveManifestHookBase[IProject]): + """Updates the manifest file after a project batch is migrated.""" + pass - __namespace__ = "Python.TestApplication" - _dotnet_base = ISyncContentBatchMigrationCompletedHook[IWorkbook] - - - def __init__(self, manifest_serializer: PyMigrationManifestSerializer, migration: IMigration) -> None: - """Default __init__.""" - self._manifest_serializer = manifest_serializer - self._migration = migration - - self._logger = logging.getLogger(self.__class__.__name__) - self._logger.setLevel(logging.DEBUG) - +class SaveDataSourceManifestHook(SaveManifestHookBase[IDataSource]): + """Updates the manifest file after a data source batch is migrated.""" + pass - def Execute(self, ctx): # noqa: N802 - """Implements Execute from base.""" - self._logger.debug("Saving manifest") - self._manifest_serializer.save(tableau_migration.migration.PyMigrationManifest(self._migration.Manifest), helper.manifest_path) \ No newline at end of file +class SaveWorkbookManifestHook(SaveManifestHookBase[IWorkbook]): + """Updates the manifest file after a workbook batch is migrated.""" + pass \ No newline at end of file diff --git a/tests/Python.TestApplication/migration_testcomponents_mappings.py b/tests/Python.TestApplication/migration_testcomponents_mappings.py index ac45a95..f17989f 100644 --- a/tests/Python.TestApplication/migration_testcomponents_mappings.py +++ b/tests/Python.TestApplication/migration_testcomponents_mappings.py @@ -17,19 +17,24 @@ import logging import helper +from typing import ( + Generic, + TypeVar +) +from tableau_migration import ( + ContentLocation, + ContentMappingBase, + ContentMappingContext, + IDataSource, + IProject, + IUser, + IWorkbook, + LicenseLevels, + TableauCloudUsernameMappingBase +) -from Tableau.Migration import ContentLocation -from Tableau.Migration.Api.Rest.Models import LicenseLevels -from Tableau.Migration.Content import IUser -from Tableau.Migration.Interop.Hooks.Mappings import ISyncContentMapping -from Tableau.Migration.Engine.Hooks.Mappings.Default import ITableauCloudUsernameMapping - -class PyTestTableauCloudUsernameMapping(ITableauCloudUsernameMapping, ISyncContentMapping[IUser] -): +class TestTableauCloudUsernameMapping(TableauCloudUsernameMappingBase): """Mapping that takes a base email and appends the source item name to the email username.""" - - __namespace__ = "Python.TestApplication" - _dotnet_base = ITableauCloudUsernameMapping def __init__(self): """Default init to set up logging.""" @@ -40,59 +45,134 @@ def __init__(self): self._base_username, self._base_domain = helper.config['test_tableau_cloud_username_options']['base_override_mail_address'].split('@') - def Execute(self,ctx): # noqa: N802 + def map(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]: # noqa: N802 """Implements Execute from base.""" - parent_domain = ctx.MappedLocation.Parent() + parent_domain = ctx.mapped_location.parent() - if self._always_override_address is False and ctx.ContentItem.Email: - ctx = ctx.MapTo(parent_domain.Append(ctx.ContentItem.Email)) + if self._always_override_address is False and ctx.content_item.email: + ctx = ctx.map_to(parent_domain.append(ctx.content_item.email)) return ctx # Takes the existing "Name" and appends the default domain to build the email - item_name: str = ctx.ContentItem.Name + item_name: str = ctx.content_item.name item_name = item_name.replace(' ', '') test_email = f'{self._base_username}+{item_name}@{self._base_domain}' - ctx = ctx.MapTo(parent_domain.Append(test_email)) - self._logger.debug('Mapped %s to %s', ctx.ContentItem.Email, ctx.MappedLocation.ToString()) + ctx = ctx.map_to(parent_domain.append(test_email)) + self._logger.debug('Mapped %s to %s', ctx.content_item.email, str(ctx.mapped_location)) return ctx -class PySpecialUserMapping(ISyncContentMapping[IUser]): +class SpecialUserMapping(ContentMappingBase[IUser]): """A class to map users to server admin.""" - __namespace__ = "Python.TestApplication" - _dotnet_base = ISyncContentMapping[IUser] - _admin_username = ContentLocation.ForUsername(helper.config['special_users']['admin_domain'], helper.config['special_users']['admin_username']) + _admin_username = ContentLocation.for_username(helper.config['special_users']['admin_domain'], helper.config['special_users']['admin_username']) def __init__(self): """Default init to set up logging.""" self._logger = logging.getLogger(self.__class__.__name__) self._logger.setLevel(logging.DEBUG) - def Execute(self,ctx): # noqa: N802 - """Implements Execute from base.""" - if ctx.ContentItem.Email in helper.config['special_users']['emails']: - ctx=ctx.MapTo(self._admin_username) - self._logger.debug('Mapped %s to %s', ctx.ContentItem.Email, ctx.MappedLocation.ToString()) + def map(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]: + if ctx.content_item.email in helper.config['special_users']['emails']: + ctx = ctx.map_to(self._admin_username) + self._logger.debug('Mapped %s to %s', ctx.content_item.email, str(ctx.mapped_location)) return ctx -class PyUnlicensedUserMapping(ISyncContentMapping[IUser]): +class UnlicensedUserMapping(ContentMappingBase[IUser]): """A class to map unlicensed users to server admin.""" - __namespace__ = "Python.TestApplication" - _dotnet_base = ISyncContentMapping[IUser] - _admin_username = ContentLocation.ForUsername(helper.config['special_users']['admin_domain'], helper.config['special_users']['admin_username']) + _admin_username = ContentLocation.for_username(helper.config['special_users']['admin_domain'], helper.config['special_users']['admin_username']) def __init__(self): - """Implements Execute from base.""" + """Default init to set up logging.""" self._logger = logging.getLogger(self.__class__.__name__) self._logger.setLevel(logging.DEBUG) - def Execute(self,ctx): # noqa: N802 - """Implements Execute from base.""" - if ctx.ContentItem.LicenseLevel == LicenseLevels.Unlicensed: - ctx=ctx.MapTo(self._admin_username) - self._logger.debug('Mapped %s to %s', ctx.ContentItem.Email, ctx.MappedLocation.ToString()) + def map(self, ctx: ContentMappingContext[IUser]) -> ContentMappingContext[IUser]: + if ctx.content_item.license_level == LicenseLevels.UNLICENSED: + ctx = ctx.map_to(self._admin_username) + self._logger.debug('Mapped %s to %s', ctx.content_item.email, str(ctx.mapped_location)) return ctx + +TContent = TypeVar("TContent") + +class _ContentWithinSkippedLocationMapping(Generic[TContent]): + """Generic base mapping wrapper for content within skipped location.""" + + def __init__(self, logger_name: str): + """Default init to set up logging.""" + self._logger = logging.getLogger(logger_name) + self._logger.setLevel(logging.DEBUG) + + def map(self, ctx: ContentMappingContext[TContent], services) -> ContentMappingContext[TContent]: + """Executes the mapping. + + Args: + ctx: The input context from the migration engine or previous hook. + + Returns: + The context, potentially modified to pass on to the next hook or migration engine, or None to continue passing the input context. + """ + if helper.config['skipped_project'] == '': + return ctx + + path_replaced = ctx.content_item.location.path.replace(helper.config['skipped_project'],'') + path_separator = ctx.content_item.location.path_separator + + if not ctx.content_item.location.path.startswith(helper.config['skipped_project']) or \ + path_replaced == '' or \ + len(path_replaced.split(path_separator)) <= 2: # considering the first empty value before the first slash + return ctx + + destination_project_finder = services.get_destination_finder(IProject) + + mapped_destination = ContentLocation.from_path(helper.config['skipped_parent_destination'], path_separator) + + project_reference = destination_project_finder.find_by_mapped_location(mapped_destination) + + if project_reference is None: + self._logger.error('Cannot map %s "%s" that belongs to "%s" to the project "%s". You must create the destination location first.', self.__orig_class__.__args__[0].__name__, ctx.content_item.name, helper.config['skipped_project'], helper.config['skipped_parent_destination']) + return ctx + + mapped_list = list(mapped_destination.path_segments) + + for i in range(len(helper.config['skipped_project'].split(ctx.content_item.location.path_separator))+1,len(ctx.content_item.location.path_segments)): + mapped_list.append(ctx.content_item.location.path_segments[i]) + + self._logger.info('Mapping the %s "%s" that belongs to "%s" to the project "%s" (Id: %s).', self.__orig_class__.__args__[0].__name__, ctx.content_item.name, helper.config['skipped_project'], helper.config['skipped_parent_destination'], project_reference.id) + + ctx = ctx.map_to(ContentLocation.from_path(path_separator.join(mapped_list),path_separator)) + + return ctx + +class ProjectWithinSkippedLocationMapping(ContentMappingBase[IProject]): + """A class to map projects within skipped project to a configured destination project.""" + + def __init__(self): + """Default init to set up wrapper.""" + self._mapper = _ContentWithinSkippedLocationMapping[IProject](self.__class__.__name__) + + def map(self, ctx: ContentMappingContext[IProject]) -> ContentMappingContext[IProject]: + return self._mapper.map(ctx, self.services) + +class DataSourceWithinSkippedLocationMapping(ContentMappingBase[IDataSource]): + """A class to map datasources within skipped project to a configured destination project.""" + + def __init__(self): + """Default init to set up wrapper.""" + self._mapper = _ContentWithinSkippedLocationMapping[IDataSource](self.__class__.__name__) + + def map(self, ctx: ContentMappingContext[IDataSource]) -> ContentMappingContext[IDataSource]: + return self._mapper.map(ctx, self.services) + +class WorkbookWithinSkippedLocationMapping(ContentMappingBase[IWorkbook]): + """A class to map workbooks within skipped project to a configured destination project.""" + + def __init__(self): + """Default init to set up wrapper.""" + self._mapper = _ContentWithinSkippedLocationMapping[IWorkbook](self.__class__.__name__) + + def map(self, ctx: ContentMappingContext[IWorkbook]) -> ContentMappingContext[IWorkbook]: + return self._mapper.map(ctx, self.services) diff --git a/tests/Python.TestApplication/migration_testcomponents_transformers.py b/tests/Python.TestApplication/migration_testcomponents_transformers.py new file mode 100644 index 0000000..0a63b90 --- /dev/null +++ b/tests/Python.TestApplication/migration_testcomponents_transformers.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024, Salesforce, Inc. +# SPDX-License-Identifier: Apache-2 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Transformers for the Python.TestApplication.""" + +from tableau_migration import ( + ContentTransformerBase, + IPublishableGroup, + IUser +) + +class RemoveMissingDestinationUsersFromGroupsTransformer(ContentTransformerBase[IPublishableGroup]): + """A class to transform groups to not try to link skipped users to the group.""" + + def transform(self, item_to_transform: IPublishableGroup) -> IPublishableGroup: + destination_user_finder = self.services.get_destination_finder(IUser) + users_list = [] + for usergroup in item_to_transform.users: + destination_reference = destination_user_finder.find_by_id(usergroup.user.id) + + if destination_reference is not None: + users_list.append(usergroup) + + item_to_transform.users = users_list + + return item_to_transform diff --git a/tests/Python.TestApplication/requirements.txt b/tests/Python.TestApplication/requirements.txt index 69d617b..2655ae5 100644 --- a/tests/Python.TestApplication/requirements.txt +++ b/tests/Python.TestApplication/requirements.txt @@ -1,6 +1,5 @@ typing_extensions==4.9.0 cffi==1.16.0 -clr-loader==0.2.6 pycparser==2.21 pythonnet==3.0.3 configparser==6.0.0 diff --git a/tests/Python.TestApplication/test_tableau_cloud_username_mapping.py b/tests/Python.TestApplication/test_tableau_cloud_username_mapping.py deleted file mode 100644 index 34f0f3e..0000000 --- a/tests/Python.TestApplication/test_tableau_cloud_username_mapping.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2024, Salesforce, Inc. -# SPDX-License-Identifier: Apache-2 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from configparser import ConfigParser -from Tableau.Migration.Interop.Hooks.Mappings import ISyncContentMapping -from Tableau.Migration.Content import IUser - -class PyTestTableauCloudUsernameMapping(ISyncContentMapping[IUser]): - __namespace__ = "MyNamespace" - - def __init__(self, config: ConfigParser) -> None: - self._config: ConfigParser = config - None - - def Execute(self, ctx): - domain = ctx.MappedLocation.Parent() - - # Re-use an existing email if it already exists unless always override is set - if self._config['TEST_TABLEAU_CLOUD_USERNAME_OPTIONS']['ALWAYS_OVERRIDE_ADDRESS'] == False and ctx.ContentItem.Email: - return ctx.MapTo(domain.Append(ctx.ContentItem.Email)) - - username, domain = ctx.ContentItem.Email.split('@') - test_email = username + ctx.ContentItem.Name.Replace(" ", "") + '@' + domain - - return ctx.MapTo(domain.Append(test_email)) - diff --git a/tests/Python.TestApplication/testapplication_tests/test_classes.py b/tests/Python.TestApplication/testapplication_tests/test_classes.py index 6be294a..17df168 100644 --- a/tests/Python.TestApplication/testapplication_tests/test_classes.py +++ b/tests/Python.TestApplication/testapplication_tests/test_classes.py @@ -30,18 +30,7 @@ from enum import Enum from typing import List -from migration_testcomponents_engine_manifest import ( - PyMigrationManifestSerializer) - -from migration_testcomponents_filters import( - PySpecialUserFilter, - PyUnlicensedUserFilter) - -from migration_testcomponents_mappings import( - PySpecialUserMapping, - PyUnlicensedUserMapping) -from migration_testcomponents_hooks import( - PyTimeLoggerAfterActionHook) +from migration_testcomponents_engine_manifest import PyMigrationManifestSerializer def get_class_methods(cls): """ @@ -198,12 +187,7 @@ def test_overloaded_missing(self): @pytest.mark.parametrize("python_class, ignored_methods", [ - (PyMigrationManifestSerializer, None), - (PySpecialUserFilter, "ExecuteAsync"), - (PySpecialUserMapping, None), - (PyUnlicensedUserMapping, None), - (PyUnlicensedUserFilter, "ExecuteAsync"), - (PyTimeLoggerAfterActionHook, None) + (PyMigrationManifestSerializer, None) ]) def test_classes(python_class, ignored_methods): """Verify that all the python wrapper classes actually wrap all the dotnet methods and properties.""" diff --git a/tests/Tableau.Migration.TestApplication/Config/TestApplicationOptions.cs b/tests/Tableau.Migration.TestApplication/Config/TestApplicationOptions.cs index 303a2ff..cf5db04 100644 --- a/tests/Tableau.Migration.TestApplication/Config/TestApplicationOptions.cs +++ b/tests/Tableau.Migration.TestApplication/Config/TestApplicationOptions.cs @@ -32,5 +32,9 @@ public sealed class TestApplicationOptions public SpecialUsersOptions SpecialUsers { get; set; } = new(); public string PreviousManifestPath { get; set; } = ""; + + public string SkippedProject { get; set; } = string.Empty; + + public string SkippedMissingParentDestination { get; set; } = "Missing Parent"; } } diff --git a/tests/Tableau.Migration.TestApplication/Hooks/ContentWithinSkippedLocationMapping.cs b/tests/Tableau.Migration.TestApplication/Hooks/ContentWithinSkippedLocationMapping.cs new file mode 100644 index 0000000..6c6c992 --- /dev/null +++ b/tests/Tableau.Migration.TestApplication/Hooks/ContentWithinSkippedLocationMapping.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Tableau.Migration.Content; +using Tableau.Migration.Engine.Endpoints.Search; +using Tableau.Migration.Engine.Hooks.Mappings; +using Tableau.Migration.TestApplication.Config; + +namespace Tableau.Migration.TestApplication.Hooks +{ + class ContentWithinSkippedLocationMapping : IContentMapping + where TContent : IContentReference + { + private readonly ContentLocation _skippedParentProject; + private readonly ContentLocation _missingParentLocation; + private readonly IDestinationContentReferenceFinder _destinationProjectContentReferenceFinder; + private readonly ILogger> _logger; + + public ContentWithinSkippedLocationMapping( + ILogger> logger, + IDestinationContentReferenceFinderFactory destinationContentReferenceFinderFactory, + IOptions options) + { + _destinationProjectContentReferenceFinder = destinationContentReferenceFinderFactory.ForDestinationContentType(); + _logger = logger; + _skippedParentProject = ContentLocation.FromPath(options.Value.SkippedProject); + _missingParentLocation = ContentLocation.FromPath(options.Value.SkippedMissingParentDestination); + } + + public async Task?> ExecuteAsync( + ContentMappingContext ctx, + CancellationToken cancel) + { + if (_skippedParentProject.IsEmpty) + { + return ctx; + } + + var pathReplaced = ctx.ContentItem.Location.Path.Replace(_skippedParentProject.Path, ""); + + if (!ctx.ContentItem.Location.Path.StartsWith(_skippedParentProject.Path) || + string.IsNullOrWhiteSpace(pathReplaced) || + pathReplaced.Split(Constants.PathSeparator, StringSplitOptions.RemoveEmptyEntries).Length <= 1) + { + return ctx; + } + + var contentReference = await _destinationProjectContentReferenceFinder + .FindByMappedLocationAsync(_missingParentLocation, cancel) + .ConfigureAwait(false); + + if (contentReference is null) + { + _logger.LogError($"Cannot map the {typeof(TContent).Name} \"{ctx.ContentItem.Name}\" that belongs to \"{nameof(TestApplicationOptions.SkippedProject)}\" to the Project \"{_missingParentLocation}\". You must create the destination location first."); + + return ctx; + } + var pathList = new List(_missingParentLocation.PathSegments); + + for (int i = _skippedParentProject.PathSegments.Length + 1; i < ctx.ContentItem.Location.PathSegments.Length; i++) + { + pathList.Add(ctx.ContentItem.Location.PathSegments[i]); + } + + _logger.LogInformation($"Mapping the {typeof(TContent).Name} \"{ctx.ContentItem.Name}\" that belongs to \"{nameof(TestApplicationOptions.SkippedProject)}\" to the Project \"{_missingParentLocation}\" ( Id: {contentReference.Id})"); + + ctx = ctx.MapTo(new ContentLocation(pathList)); + + return ctx; + } + } +} diff --git a/tests/Tableau.Migration.TestApplication/Hooks/NonDomainUserFilter.cs b/tests/Tableau.Migration.TestApplication/Hooks/NonDomainUserFilter.cs index b8b5448..eb39925 100644 --- a/tests/Tableau.Migration.TestApplication/Hooks/NonDomainUserFilter.cs +++ b/tests/Tableau.Migration.TestApplication/Hooks/NonDomainUserFilter.cs @@ -19,16 +19,15 @@ using System.Collections.Generic; using System.DirectoryServices; using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tableau.Migration.Api.Rest.Models; using Tableau.Migration.Content; using Tableau.Migration.Engine; using Tableau.Migration.Engine.Hooks.Filters; -using Tableau.Migration.TestApplication.Config; -using System.Runtime.InteropServices; -using Microsoft.Extensions.Logging; -using Polly; -using Microsoft.Extensions.Options; using Tableau.Migration.Resources; +using Tableau.Migration.TestApplication.Config; namespace Tableau.Migration.TestApplication.Hooks { diff --git a/tests/Tableau.Migration.TestApplication/Hooks/RemoveMissingDestinationUsersFromGroupsTransformer.cs b/tests/Tableau.Migration.TestApplication/Hooks/RemoveMissingDestinationUsersFromGroupsTransformer.cs new file mode 100644 index 0000000..c37a7ec --- /dev/null +++ b/tests/Tableau.Migration.TestApplication/Hooks/RemoveMissingDestinationUsersFromGroupsTransformer.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Content; +using Tableau.Migration.Engine.Endpoints.Search; +using Tableau.Migration.Engine.Hooks.Transformers; +using Tableau.Migration.Resources; + +namespace Tableau.Migration.TestApplication.Hooks +{ + class RemoveMissingDestinationUsersFromGroupsTransformer : ContentTransformerBase + { + private readonly IDestinationContentReferenceFinder _destinationUserContentReferenceFinder; + + public RemoveMissingDestinationUsersFromGroupsTransformer( + ISharedResourcesLocalizer? localizer, + ILogger>? logger, + IDestinationContentReferenceFinderFactory destinationContentReferenceFinderFactory) + : base(localizer, logger) + { + _destinationUserContentReferenceFinder = destinationContentReferenceFinderFactory.ForDestinationContentType(); + } + + public override async Task TransformAsync( + IPublishableGroup itemToTransform, + CancellationToken cancel) + { + var updatedUsersList = new List(); + + foreach (var groupUser in itemToTransform.Users) + { + var destinationReference = await _destinationUserContentReferenceFinder.FindByIdAsync( + groupUser.User.Id, + cancel). + ConfigureAwait(false); + + if (destinationReference is not null) + { + updatedUsersList.Add(groupUser); + } + } + + itemToTransform.Users = updatedUsersList; + + return itemToTransform; + } + } +} diff --git a/tests/Tableau.Migration.TestApplication/Hooks/SaveManifestAfterBatchMigrationCompletedHook.cs b/tests/Tableau.Migration.TestApplication/Hooks/SaveManifestAfterBatchMigrationCompletedHook.cs index 5ce0b12..0361492 100644 --- a/tests/Tableau.Migration.TestApplication/Hooks/SaveManifestAfterBatchMigrationCompletedHook.cs +++ b/tests/Tableau.Migration.TestApplication/Hooks/SaveManifestAfterBatchMigrationCompletedHook.cs @@ -15,14 +15,11 @@ // limitations under the License. // -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; using System.Threading; using System.Threading.Tasks; -using Tableau.Migration.Content; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tableau.Migration.Engine; -using Tableau.Migration.Engine.Actions; using Tableau.Migration.Engine.Hooks; using Tableau.Migration.TestApplication.Config; using Tableau.Migration.TestComponents.Engine.Manifest; diff --git a/tests/Tableau.Migration.TestApplication/Hooks/SkipByParentLocationFilter.cs b/tests/Tableau.Migration.TestApplication/Hooks/SkipByParentLocationFilter.cs new file mode 100644 index 0000000..86f40e2 --- /dev/null +++ b/tests/Tableau.Migration.TestApplication/Hooks/SkipByParentLocationFilter.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Tableau.Migration.Content; +using Tableau.Migration.Engine; +using Tableau.Migration.Engine.Endpoints.Search; +using Tableau.Migration.Engine.Hooks.Filters; +using Tableau.Migration.TestApplication.Config; + +namespace Tableau.Migration.TestApplication.Hooks +{ + class SkipByParentLocationFilter : IContentFilter + where TContent : IContentReference + { + private readonly ContentLocation _skippedParentProject; + private readonly ISourceContentReferenceFinder _sourceProjectContentReferenceFinder; + private readonly ILogger> _logger; + + public SkipByParentLocationFilter( + ILogger> logger, + ISourceContentReferenceFinderFactory sourceContentReferenceFinderFactory, + IOptions options) + { + _sourceProjectContentReferenceFinder = sourceContentReferenceFinderFactory.ForSourceContentType(); + _logger = logger; + _skippedParentProject = ContentLocation.FromPath(options.Value.SkippedProject); + } + + public async Task>?> ExecuteAsync( + IEnumerable> ctx, + CancellationToken cancel) + { + if (_skippedParentProject.IsEmpty) + { + return ctx; + } + + var filteredList = new List>(); + + foreach (var item in ctx) + { + var parent = item.SourceItem.Location.Parent(); + + if (_skippedParentProject != parent) + { + filteredList.Add(item); + continue; + } + + var contentReference = await _sourceProjectContentReferenceFinder + .FindBySourceLocationAsync(parent, cancel) + .ConfigureAwait(false); + + _logger.LogInformation($"Skipping {typeof(TContent).Name} that belongs to \"{nameof(TestApplicationOptions.SkippedProject)}\" (Project Id: {contentReference?.Id})"); + } + + return filteredList; + } + } +} diff --git a/tests/Tableau.Migration.TestApplication/Hooks/SpecialUserFilter.cs b/tests/Tableau.Migration.TestApplication/Hooks/SpecialUserFilter.cs index 4851472..32ca8a0 100644 --- a/tests/Tableau.Migration.TestApplication/Hooks/SpecialUserFilter.cs +++ b/tests/Tableau.Migration.TestApplication/Hooks/SpecialUserFilter.cs @@ -16,19 +16,14 @@ // using System; -using System.Collections.Generic; -using System.DirectoryServices; using System.Linq; -using Tableau.Migration.Api.Rest.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tableau.Migration.Content; using Tableau.Migration.Engine; using Tableau.Migration.Engine.Hooks.Filters; -using Tableau.Migration.TestApplication.Config; -using System.Runtime.InteropServices; -using Microsoft.Extensions.Logging; -using Polly; using Tableau.Migration.Resources; -using Microsoft.Extensions.Options; +using Tableau.Migration.TestApplication.Config; namespace Tableau.Migration.TestApplication.Hooks { diff --git a/tests/Tableau.Migration.TestApplication/Hooks/SpecialUserMapping.cs b/tests/Tableau.Migration.TestApplication/Hooks/SpecialUserMapping.cs index e7adcec..fca18fb 100644 --- a/tests/Tableau.Migration.TestApplication/Hooks/SpecialUserMapping.cs +++ b/tests/Tableau.Migration.TestApplication/Hooks/SpecialUserMapping.cs @@ -16,15 +16,15 @@ // using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tableau.Migration.Content; using Tableau.Migration.Engine.Hooks.Mappings; using Tableau.Migration.Resources; using Tableau.Migration.TestApplication.Config; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Logging; -using System.Linq; namespace Tableau.Migration.TestApplication.Hooks { diff --git a/tests/Tableau.Migration.TestApplication/Program.cs b/tests/Tableau.Migration.TestApplication/Program.cs index 3b56784..c578b70 100644 --- a/tests/Tableau.Migration.TestApplication/Program.cs +++ b/tests/Tableau.Migration.TestApplication/Program.cs @@ -105,7 +105,10 @@ public static async Task Main(string[] args) .AddScoped() .AddScoped() .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped(typeof(SkipByParentLocationFilter<>)) + .AddScoped(typeof(ContentWithinSkippedLocationMapping<>)) + .AddScoped(); }) .Build(); diff --git a/tests/Tableau.Migration.TestApplication/TestApplication.cs b/tests/Tableau.Migration.TestApplication/TestApplication.cs index 4783910..54bd5d3 100644 --- a/tests/Tableau.Migration.TestApplication/TestApplication.cs +++ b/tests/Tableau.Migration.TestApplication/TestApplication.cs @@ -106,6 +106,13 @@ public async Task StartAsync(CancellationToken cancel) // Log when a content type is done _planBuilder.Hooks.Add(); + _planBuilder.Filters.Add, IProject>(); + _planBuilder.Filters.Add, IDataSource>(); + _planBuilder.Filters.Add, IWorkbook>(); + _planBuilder.Transformers.Add(); + _planBuilder.Mappings.Add, IProject>(); + _planBuilder.Mappings.Add, IDataSource>(); + _planBuilder.Mappings.Add, IWorkbook>(); // Skip content types we've already done. // Uncomment as needed //_planBuilder.Filters.Add(new SkipFilter()); diff --git a/tests/Tableau.Migration.TestComponents/JsonConverters/ExceptionJsonConverter.cs b/tests/Tableau.Migration.TestComponents/JsonConverters/ExceptionJsonConverter.cs index 76b82de..380c78a 100644 --- a/tests/Tableau.Migration.TestComponents/JsonConverters/ExceptionJsonConverter.cs +++ b/tests/Tableau.Migration.TestComponents/JsonConverters/ExceptionJsonConverter.cs @@ -15,10 +15,12 @@ // limitations under the License. // -using Microsoft.Extensions.Logging; +using System.Collections.Immutable; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using Python.Runtime; namespace Tableau.Migration.TestComponents.JsonConverters { @@ -28,7 +30,12 @@ namespace Tableau.Migration.TestComponents.JsonConverters /// public class ExceptionJsonConverter : JsonConverter { - ILogger _logger; + private readonly ILogger _logger; + private static readonly ImmutableHashSet IGNORED_PROPERTY_TYPES = new[] + { + typeof(Type), + typeof(CancellationToken) + }.ToImmutableHashSet(); public ExceptionJsonConverter(ILogger logger) { @@ -46,10 +53,19 @@ public override void Write(Utf8JsonWriter writer, Exception value, JsonSerialize { var exceptionType = value.GetType(); + if (value is PythonException pyException) + { + writer.WriteStartObject(); + writer.WriteString("ClassName", exceptionType.FullName); + writer.WriteString("Message", pyException.Format()); + writer.WriteEndObject(); + return; + } + var properties = exceptionType.GetProperties() - .Where(e => e.PropertyType != typeof(Type)) + .Where(e => !IGNORED_PROPERTY_TYPES.Contains(e.PropertyType)) .Where(e => e.PropertyType.Namespace != typeof(MemberInfo).Namespace) - .ToList(); + .ToImmutableArray(); writer.WriteStartObject(); writer.WriteString("ClassName", exceptionType.FullName); @@ -73,6 +89,7 @@ public override void Write(Utf8JsonWriter writer, Exception value, JsonSerialize catch (Exception ex) { _logger.LogError(ex, "Unable to write property"); + throw new Exception($"Error serializing {exceptionType.FullName}.{property.Name} (type {property.PropertyType}).", ex); } } diff --git a/tests/Tableau.Migration.TestComponents/Tableau.Migration.TestComponents.csproj b/tests/Tableau.Migration.TestComponents/Tableau.Migration.TestComponents.csproj index b56b7df..51729f7 100644 --- a/tests/Tableau.Migration.TestComponents/Tableau.Migration.TestComponents.csproj +++ b/tests/Tableau.Migration.TestComponents/Tableau.Migration.TestComponents.csproj @@ -8,6 +8,7 @@ + diff --git a/tests/Tableau.Migration.Tests/AutoFixtureTestBase.cs b/tests/Tableau.Migration.Tests/AutoFixtureTestBase.cs index 9960329..4555367 100644 --- a/tests/Tableau.Migration.Tests/AutoFixtureTestBase.cs +++ b/tests/Tableau.Migration.Tests/AutoFixtureTestBase.cs @@ -17,15 +17,10 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading; using AutoFixture; -using AutoFixture.AutoMoq; using AutoFixture.Kernel; -using Moq; -using Tableau.Migration.Api.Rest.Models.Requests; -using Tableau.Migration.Api.Rest.Models.Responses; namespace Tableau.Migration.Tests { @@ -43,8 +38,6 @@ public abstract class AutoFixtureTestBase public AutoFixtureTestBase() { - Customize(); - var testCancellationTimeoutConfig = Environment.GetEnvironmentVariable("MIGRATIONSDK_TEST_CANCELLATION_TIMEOUT_TIMESPAN"); if (!TimeSpan.TryParse(testCancellationTimeoutConfig, out TestCancellationTimeout)) @@ -56,7 +49,7 @@ public AutoFixtureTestBase() /// /// Creates a new instance. /// - protected static IFixture CreateFixture() => new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + protected static IFixture CreateFixture() => FixtureFactory.Create(); /// /// Creates a variable of the requested type. @@ -134,123 +127,5 @@ protected IEnumerable CreateMany(int generatedCount, IEnumerable? first /// The value to freeze. /// The value that will subsequently always be created for . protected T Freeze(T value) => AutoFixture.Freeze(composer => composer.FromFactory(() => value)); - - private void Customize() - { - AutoFixture.Register(() => Create().Object); - - AutoFixture.Register(() => Create>().Object); - - AutoFixture.Register(() => new ContentLocation(CreateMany())); - - AutoFixture.Register((string data) => - { - var bytes = Constants.DefaultEncoding.GetBytes(data); - return new MemoryStream(bytes); - }); - - AutoFixture.Register(() => MemoryStreamManager.Instance); - - #region - JobResponse - - - // These properties should return DateTime strings instead of the default Guid-like ones. - AutoFixture.Customize(composer => composer - .With(j => j.CreatedAt, () => Create().ToIso8601()) - .With(j => j.UpdatedAt, () => Create().ToIso8601()) - .With(j => j.CompletedAt, () => Create()?.ToIso8601())); - - #endregion - - #region - ImportJobResponse - - - // These properties should return DateTime strings instead of the default Guid-like ones. - AutoFixture.Customize(composer => composer - .With(j => j.CreatedAt, () => Create().ToIso8601())); - - #endregion - - #region - UsersResponse - - - // Just to make the strings a little easier to read during test debugging - AutoFixture.Customize(composer => composer - .With(d => d.Name, $"DomainName{Guid.NewGuid()}")); - - // Wrong - Work item in in backlog - // The domain does not go into the name for UsersResponse.UserType. Also, domain can never be "local" - // here. If code tries something like Create.With(u => domain.name, "local"), then this code is skipped - AutoFixture.Customize(composer => composer - .With( - u => u.Name, - (UsersResponse.UserType.DomainType domain) => - { - var plainUserName = $"Name{Guid.NewGuid()}"; - var domainName = domain?.Name; - - return string.Equals(domainName, "local", StringComparison.OrdinalIgnoreCase) - ? plainUserName - : $"{domainName}{Constants.DomainNameSeparator}{plainUserName}"; - })); - - #endregion - - #region - CreateProjectResponse - - - // These properties should return Guid strings instead of the default PropertyName/Guid ones. - AutoFixture.Customize(composer => composer - .With(p => p.ParentProjectId, () => Create()?.ToString())); - - #endregion - - #region - ProjectsResponse - - - // These properties should return Guid strings instead of the default PropertyName/Guid ones. - AutoFixture.Customize(composer => composer - .With(p => p.ParentProjectId, () => Create()?.ToString())); - - #endregion - - #region - UpdateDataSourceResponse - - - // These properties should return DateTime strings instead of the default Guid-like ones. - AutoFixture.Customize(composer => composer - .With(j => j.CreatedAt, () => Create().ToIso8601()) - .With(j => j.UpdatedAt, () => Create().ToIso8601())); - - #endregion - - #region - UpdateWorkbookResponse - - - // These properties should return DateTime strings instead of the default Guid-like ones. - AutoFixture.Customize(composer => composer - .With(j => j.CreatedAt, () => Create().ToIso8601()) - .With(j => j.UpdatedAt, () => Create().ToIso8601())); - - #endregion - - #region - UpdateConnectionRequest - - - // These properties should return nullable bool strings instead of the default Guid-like ones. - AutoFixture.Customize(composer => composer - .With(j => j.EmbedPassword, () => Create().ToString()) - .With(j => j.QueryTaggingEnabled, () => Create().ToString())); - - #endregion - - #region - ConnectionsResponse - - - // These properties should return nullable bool strings instead of the default Guid-like ones. - AutoFixture.Customize(composer => composer - .With(j => j.QueryTaggingEnabled, () => Create().ToString())); - - #endregion - - #region - ConnectionResponse - - - // These properties should return nullable bool strings instead of the default Guid-like ones. - AutoFixture.Customize(composer => composer - .With(j => j.QueryTaggingEnabled, () => Create().ToString())); - - #endregion - } } } diff --git a/tests/Tableau.Migration.Tests/FixtureFactory.cs b/tests/Tableau.Migration.Tests/FixtureFactory.cs new file mode 100644 index 0000000..3b65fff --- /dev/null +++ b/tests/Tableau.Migration.Tests/FixtureFactory.cs @@ -0,0 +1,156 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.IO; +using AutoFixture; +using AutoFixture.AutoMoq; +using Moq; +using Tableau.Migration.Api.Rest.Models.Requests; +using Tableau.Migration.Api.Rest.Models.Responses; + +namespace Tableau.Migration.Tests +{ + public static class FixtureFactory + { + public static IFixture Create() => Customize(new Fixture()); + + private static IFixture Customize(IFixture fixture) + { + fixture = fixture.Customize(new AutoMoqCustomization { ConfigureMembers = true }); + + fixture.Customizations.Add(new ImmutableCollectionSpecimenBuilder()); + + fixture.Register(() => fixture.Create().Object); + + fixture.Register(() => fixture.Create>().Object); + + fixture.Register(() => new ContentLocation(fixture.CreateMany())); + + fixture.Register((string data) => + { + var bytes = Constants.DefaultEncoding.GetBytes(data); + return new MemoryStream(bytes); + }); + + fixture.Register(() => MemoryStreamManager.Instance); + + #region - JobResponse - + + // These properties should return DateTime strings instead of the default Guid-like ones. + fixture.Customize(composer => composer + .With(j => j.CreatedAt, () => fixture.Create().ToIso8601()) + .With(j => j.UpdatedAt, () => fixture.Create().ToIso8601()) + .With(j => j.CompletedAt, () => fixture.Create()?.ToIso8601())); + + #endregion + + #region - ImportJobResponse - + + // These properties should return DateTime strings instead of the default Guid-like ones. + fixture.Customize(composer => composer + .With(j => j.CreatedAt, () => fixture.Create().ToIso8601())); + + #endregion + + #region - UsersResponse - + + // Just to make the strings a little easier to read during test debugging + fixture.Customize(composer => composer + .With(d => d.Name, $"DomainName{Guid.NewGuid()}")); + + // Wrong - Work item in in backlog + // The domain does not go into the name for UsersResponse.UserType. Also, domain can never be "local" + // here. If code tries something like Create.With(u => domain.name, "local"), then this code is skipped + fixture.Customize(composer => composer + .With( + u => u.Name, + (UsersResponse.UserType.DomainType domain) => + { + var plainUserName = $"Name{Guid.NewGuid()}"; + var domainName = domain?.Name; + + return string.Equals(domainName, "local", StringComparison.OrdinalIgnoreCase) + ? plainUserName + : $"{domainName}{Constants.DomainNameSeparator}{plainUserName}"; + })); + + #endregion + + #region - CreateProjectResponse - + + // These properties should return Guid strings instead of the default PropertyName/Guid ones. + fixture.Customize(composer => composer + .With(p => p.ParentProjectId, () => fixture.Create()?.ToString())); + + #endregion + + #region - ProjectsResponse - + + // These properties should return Guid strings instead of the default PropertyName/Guid ones. + fixture.Customize(composer => composer + .With(p => p.ParentProjectId, () => fixture.Create()?.ToString())); + + #endregion + + #region - UpdateDataSourceResponse - + + // These properties should return DateTime strings instead of the default Guid-like ones. + fixture.Customize(composer => composer + .With(j => j.CreatedAt, () => fixture.Create().ToIso8601()) + .With(j => j.UpdatedAt, () => fixture.Create().ToIso8601())); + + #endregion + + #region - UpdateWorkbookResponse - + + // These properties should return DateTime strings instead of the default Guid-like ones. + fixture.Customize(composer => composer + .With(j => j.CreatedAt, () => fixture.Create().ToIso8601()) + .With(j => j.UpdatedAt, () => fixture.Create().ToIso8601())); + + #endregion + + #region - UpdateConnectionRequest - + + // These properties should return nullable bool strings instead of the default Guid-like ones. + fixture.Customize(composer => composer + .With(j => j.EmbedPassword, () => fixture.Create().ToString()) + .With(j => j.QueryTaggingEnabled, () => fixture.Create().ToString())); + + #endregion + + #region - ConnectionsResponse - + + // These properties should return nullable bool strings instead of the default Guid-like ones. + fixture.Customize(composer => composer + .With(j => j.QueryTaggingEnabled, () => fixture.Create().ToString())); + + #endregion + + #region - ConnectionResponse - + + // These properties should return nullable bool strings instead of the default Guid-like ones. + fixture.Customize(composer => composer + .With(j => j.QueryTaggingEnabled, () => fixture.Create().ToString())); + + #endregion + + return fixture; + } + } +} diff --git a/tests/Tableau.Migration.Tests/ImmutableCollectionSpecimenBuilder.cs b/tests/Tableau.Migration.Tests/ImmutableCollectionSpecimenBuilder.cs new file mode 100644 index 0000000..93f3afa --- /dev/null +++ b/tests/Tableau.Migration.Tests/ImmutableCollectionSpecimenBuilder.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using AutoFixture.Kernel; + +namespace Tableau.Migration.Tests +{ + internal sealed class ImmutableCollectionSpecimenBuilder : ISpecimenBuilder + { + private static Type? GetConcreteImmutableType(Type genericType) + { + if (genericType == typeof(IImmutableList<>)) + { + return typeof(ImmutableList<>); + } + + return null; + } + + public object Create(object request, ISpecimenContext context) + { + if(!(request is Type t) || !t.IsGenericType) + { + return new NoSpecimen(); + } + + var typeArguments = t.GetGenericArguments(); + var genericType = t.GetGenericTypeDefinition(); + + if(genericType == typeof(ImmutableList<>) || genericType == typeof(IImmutableList<>)) + { + dynamic list = context.Resolve(typeof(List<>).MakeGenericType(typeArguments)); + return ImmutableList.ToImmutableList(list); + } + + return new NoSpecimen(); + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/Api/GroupsApiClientTests.cs b/tests/Tableau.Migration.Tests/Unit/Api/GroupsApiClientTests.cs index 6942cc1..738b985 100644 --- a/tests/Tableau.Migration.Tests/Unit/Api/GroupsApiClientTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Api/GroupsApiClientTests.cs @@ -302,6 +302,7 @@ public async Task Returns_success() var getUsersResponse = AutoFixture.CreateResponse(); getUsersResponse.Items = Array.Empty(); + getUsersResponse.Pagination = new() { PageNumber = 1, PageSize = int.MaxValue, TotalAvailable = getUsersResponse.Items.Length }; MockHttpClient.SetupResponse(new MockHttpResponseMessage(getUsersResponse)); @@ -332,6 +333,7 @@ public async Task Succeeds_when_group_exists() var getUsersResponse = AutoFixture.CreateResponse(); getUsersResponse.Items = Array.Empty(); + getUsersResponse.Pagination = new() { PageNumber = 1, PageSize = int.MaxValue, TotalAvailable = getUsersResponse.Items.Length }; MockHttpClient.SetupResponse(new MockHttpResponseMessage(getUsersResponse)); diff --git a/tests/Tableau.Migration.Tests/Unit/Content/Search/ContentReferenceCacheBaseTests.cs b/tests/Tableau.Migration.Tests/Unit/Content/Search/ContentReferenceCacheBaseTests.cs index c6762b1..822597c 100644 --- a/tests/Tableau.Migration.Tests/Unit/Content/Search/ContentReferenceCacheBaseTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Content/Search/ContentReferenceCacheBaseTests.cs @@ -166,6 +166,26 @@ public async Task NotFoundCachedAsync() Assert.Equal(1, Cache.SearchCalls); } + [Fact] + public async Task NotFoundSingleListAsync() + { + var notFoundItem1 = Create(); + var notFoundItem2 = Create(); + + var result = await Cache.ForLocationAsync(notFoundItem1.Location, Cancel); + Assert.Null(result); + + result = await Cache.ForLocationAsync(notFoundItem1.Location, Cancel); + Assert.Null(result); + + Assert.Equal(1, Cache.SearchCalls); + + result = await Cache.ForLocationAsync(notFoundItem2.Location, Cancel); + Assert.Null(result); + + Assert.Equal(1, Cache.SearchCalls); + } + // TODO: W-14187810 - Fix Flaky Test. // Increasing the timeout configuration helps when the test runs in a machine with limited resources. [Fact] diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderFactoryTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderFactoryTests.cs index 4c43746..6bcd331 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderFactoryTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderFactoryTests.cs @@ -16,6 +16,7 @@ // using Moq; +using Tableau.Migration.Content.Search; using Tableau.Migration.Engine.Endpoints.Search; using Xunit; @@ -34,7 +35,10 @@ public void ReturnsManifestDestinationFinder() var finder = fac.ForContentType(); - provider.Verify(x => x.GetService(typeof(ManifestDestinationContentReferenceFinder)), Times.Once); + provider.Verify(x => x.GetService(typeof(IDestinationContentReferenceFinder)), Times.Once); + + Assert.IsAssignableFrom>(finder); + Assert.IsAssignableFrom>(finder); } } } diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderTests.cs index 09b34ec..368d5f7 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestDestinationContentReferenceFinderTests.cs @@ -72,7 +72,7 @@ public async Task FindsWithCachedMappedLocationAsync() MockCache.Setup(x => x.ForLocationAsync(mappedLoc, Cancel)) .ReturnsAsync(cacheItem); - var result = await Finder.FindDestinationReferenceAsync(sourceItem.Location, Cancel); + var result = await Finder.FindBySourceLocationAsync(sourceItem.Location, Cancel); Assert.Same(cacheItem, result); @@ -93,7 +93,7 @@ public async Task FindsById() MockCache.Setup(x => x.ForLocationAsync(sourceItem.Location, Cancel)) .ReturnsAsync(cacheItem); - var result = await Finder.FindDestinationReferenceAsync(sourceItem.Id, Cancel); + var result = await Finder.FindBySourceIdAsync(sourceItem.Id, Cancel); Assert.Same(cacheItem, result); @@ -103,7 +103,7 @@ public async Task FindsById() [Fact] public async Task ReturnsNullWhenLocationNotFound() { - var result = await Finder.FindDestinationReferenceAsync(Create(), Cancel); + var result = await Finder.FindBySourceLocationAsync(Create(), Cancel); Assert.Null(result); @@ -113,7 +113,7 @@ public async Task ReturnsNullWhenLocationNotFound() [Fact] public async Task ReturnsNullWhenIdNotFound() { - var result = await Finder.FindDestinationReferenceAsync(Create(), Cancel); + var result = await Finder.FindBySourceIdAsync(Create(), Cancel); Assert.Null(result); @@ -134,7 +134,7 @@ public async Task FindsByContentUrl() MockCache.Setup(x => x.ForLocationAsync(sourceItem.Location, Cancel)) .ReturnsAsync(cacheItem); - var result = await Finder.FindDestinationReferenceAsync(sourceItem.ContentUrl, Cancel); + var result = await Finder.FindBySourceContentUrlAsync(sourceItem.ContentUrl, Cancel); Assert.Same(cacheItem, result); @@ -144,7 +144,7 @@ public async Task FindsByContentUrl() [Fact] public async Task ReturnsNullWhenContentUrlNotFound() { - var result = await Finder.FindDestinationReferenceAsync(Create(), Cancel); + var result = await Finder.FindBySourceContentUrlAsync(Create(), Cancel); Assert.Null(result); @@ -179,7 +179,7 @@ public async Task FindsWithMappedLocationFromManifestAsync() await entryBuilder.MapEntriesAsync(new[] { sourceItem }, mockMapping.Object, Cancel); entries.Single().DestinationFound(mockDestinationRef.Object); - var result = await Finder.FindMappedDestinationReferenceAsync(mappedLoc, Cancel); + var result = await Finder.FindByMappedLocationAsync(mappedLoc, Cancel); Assert.Same(mockDestinationRef.Object, result); @@ -207,7 +207,7 @@ public async Task FindsWithMappedLocationNoDestinationAsync() MockCache.Setup(x => x.ForLocationAsync(mappedLoc, Cancel)) .ReturnsAsync(cacheItem); - var result = await Finder.FindMappedDestinationReferenceAsync(mappedLoc, Cancel); + var result = await Finder.FindByMappedLocationAsync(mappedLoc, Cancel); Assert.Same(cacheItem, result); @@ -230,7 +230,7 @@ public async Task FindsWithCachedDestinationLocationAsync() MockCache.Setup(x => x.ForLocationAsync(mappedLoc, Cancel)) .ReturnsAsync(cacheItem); - var result = await Finder.FindMappedDestinationReferenceAsync(entry.MappedLocation, Cancel); + var result = await Finder.FindByMappedLocationAsync(entry.MappedLocation, Cancel); Assert.Same(cacheItem, result); diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderFactoryTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderFactoryTests.cs index beaafd7..4a4302d 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderFactoryTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderFactoryTests.cs @@ -16,6 +16,7 @@ // using Moq; +using Tableau.Migration.Content.Search; using Tableau.Migration.Engine.Endpoints.Search; using Xunit; @@ -34,7 +35,10 @@ public void ReturnsManifestSourceFinder() var finder = fac.ForContentType(); - provider.Verify(x => x.GetService(typeof(ManifestSourceContentReferenceFinder)), Times.Once); + provider.Verify(x => x.GetService(typeof(ISourceContentReferenceFinder)), Times.Once); + + Assert.IsAssignableFrom>(finder); + Assert.IsAssignableFrom>(finder); } } } diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderTests.cs index 4ea91bf..f409a7b 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Endpoints/Search/ManifestSourceContentReferenceFinderTests.cs @@ -19,7 +19,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Castle.Components.DictionaryAdapter.Xml; using Moq; using Tableau.Migration.Content.Search; using Tableau.Migration.Engine.Endpoints.Search; @@ -45,7 +44,10 @@ public ManifestSourceContentReferenceFinderTest() Pipeline = Create>(); ContentReferenceCache = Create>(); - ContentReferenceCache.Setup(x => x.ForIdAsync(It.IsAny(), It.IsAny())) + ContentReferenceCache.Setup(x => x.ForIdAsync(It.IsAny(), Cancel)) + .Returns(Task.FromResult(null)); + + ContentReferenceCache.Setup(x => x.ForLocationAsync(It.IsAny(), Cancel)) .Returns(Task.FromResult(null)); Pipeline.Setup(x => x.CreateSourceCache()) @@ -77,13 +79,13 @@ public async Task FindCacheReferenceAsync() { var sourceItem = Create(); - ContentReferenceCache.Setup(x => x.ForIdAsync(sourceItem.Id, It.IsAny())) + ContentReferenceCache.Setup(x => x.ForIdAsync(sourceItem.Id, Cancel)) .ReturnsAsync(sourceItem); var result = await Finder.FindByIdAsync(sourceItem.Id, Cancel); Assert.Same(sourceItem, result); - ContentReferenceCache.Verify(x => x.ForIdAsync(It.IsAny(), It.IsAny()), Times.Once); + ContentReferenceCache.Verify(x => x.ForIdAsync(It.IsAny(), Cancel), Times.Once); } [Fact] @@ -94,7 +96,50 @@ public async Task NotFoundAsync() var result = await Finder.FindByIdAsync(sourceItem.Id, Cancel); Assert.Null(result); - ContentReferenceCache.Verify(x => x.ForIdAsync(It.IsAny(), It.IsAny()), Times.Once); + ContentReferenceCache.Verify(x => x.ForIdAsync(It.IsAny(), Cancel), Times.Once); + } + } + + public class FindBySourceLocationAsync : ManifestSourceContentReferenceFinderTest + { + [Fact] + public async Task FindsManifestReferenceAsync() + { + var sourceItem = Create(); + + var entry = Manifest.Entries.GetOrCreatePartition().GetEntryBuilder(1) + .CreateEntries(new[] { sourceItem }, (i, e) => e) + .Single(); + + var result = await Finder.FindBySourceLocationAsync(sourceItem.Location, Cancel); + + Assert.Same(entry.Source, result); + ContentReferenceCache.Verify(x => x.ForLocationAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task FindCacheReferenceAsync() + { + var sourceItem = Create(); + + ContentReferenceCache.Setup(x => x.ForLocationAsync(sourceItem.Location, Cancel)) + .ReturnsAsync(sourceItem); + + var result = await Finder.FindBySourceLocationAsync(sourceItem.Location, Cancel); + + Assert.Same(sourceItem, result); + ContentReferenceCache.Verify(x => x.ForLocationAsync(It.IsAny(), Cancel), Times.Once); + } + + [Fact] + public async Task NotFoundAsync() + { + var sourceItem = Create(); + + var result = await Finder.FindBySourceLocationAsync(sourceItem.Location, Cancel); + + Assert.Null(result); + ContentReferenceCache.Verify(x => x.ForLocationAsync(It.IsAny(), Cancel), Times.Once); } } } diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/MappedUserTransformerTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/MappedUserTransformerTests.cs index c51e4ea..8b2b795 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/MappedUserTransformerTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/MappedUserTransformerTests.cs @@ -21,7 +21,6 @@ using Tableau.Migration.Content; using Tableau.Migration.Engine.Endpoints.Search; using Tableau.Migration.Engine.Hooks.Transformers.Default; -using Tableau.Migration.Engine.Pipelines; using Xunit; namespace Tableau.Migration.Tests.Unit.Engine.Hooks.Transformers.Default @@ -30,18 +29,18 @@ public class MappedUserTransformerTests { public abstract class MappedUserTransformerTest : AutoFixtureTestBase { - protected readonly Mock MockMigrationPipeline = new(); + protected readonly Mock MockDestinationFinderFactory = new(); protected readonly Mock> MockLogger = new(); protected readonly MockSharedResourcesLocalizer MockSharedResourcesLocalizer = new(); - protected readonly Mock> MockUserContentFinder = new(); + protected readonly Mock> MockUserContentFinder = new(); protected readonly MappedUserTransformer Transformer; public MappedUserTransformerTest() { - MockMigrationPipeline.Setup(p => p.CreateDestinationFinder()).Returns(MockUserContentFinder.Object); + MockDestinationFinderFactory.Setup(p => p.ForDestinationContentType()).Returns(MockUserContentFinder.Object); - Transformer = new(MockMigrationPipeline.Object, MockLogger.Object, MockSharedResourcesLocalizer.Object); + Transformer = new(MockDestinationFinderFactory.Object, MockLogger.Object, MockSharedResourcesLocalizer.Object); } } @@ -61,7 +60,7 @@ public async Task Returns_destination_user_when_found() var sourceUser = Create(); var destinationUser = Create(); - MockUserContentFinder.Setup(f => f.FindDestinationReferenceAsync(sourceUser.Location, Cancel)).ReturnsAsync(destinationUser); + MockUserContentFinder.Setup(f => f.FindBySourceLocationAsync(sourceUser.Location, Cancel)).ReturnsAsync(destinationUser); var result = await Transformer.ExecuteAsync(sourceUser, Cancel); diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/PermissionsTransformerTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/PermissionsTransformerTests.cs index ea9e177..e22d861 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/PermissionsTransformerTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/PermissionsTransformerTests.cs @@ -29,7 +29,6 @@ using Tableau.Migration.Content.Permissions; using Tableau.Migration.Engine.Endpoints.Search; using Tableau.Migration.Engine.Hooks.Transformers.Default; -using Tableau.Migration.Engine.Pipelines; using Tableau.Migration.Resources; using Tableau.Migration.Tests.Content.Permissions; using Xunit; @@ -42,9 +41,9 @@ public abstract class PermissionsTransformerTest : AutoFixtureTestBase { internal readonly IGranteeCapabilityComparer GranteeCapabilityComparer = new(false); - protected readonly Mock MockMigrationPipeline = new(); - protected readonly Mock> MockUserContentFinder = new(); - protected readonly Mock> MockGroupContentFinder = new(); + protected readonly Mock MockDestinationFinderFactory = new(); + protected readonly Mock> MockUserContentFinder = new(); + protected readonly Mock> MockGroupContentFinder = new(); protected readonly PermissionsTransformer Transformer; @@ -53,10 +52,10 @@ public abstract class PermissionsTransformerTest : AutoFixtureTestBase public PermissionsTransformerTest() { - MockMigrationPipeline.Setup(p => p.CreateDestinationFinder()).Returns(MockUserContentFinder.Object); - MockMigrationPipeline.Setup(p => p.CreateDestinationFinder()).Returns(MockGroupContentFinder.Object); + MockDestinationFinderFactory.Setup(p => p.ForDestinationContentType()).Returns(MockUserContentFinder.Object); + MockDestinationFinderFactory.Setup(p => p.ForDestinationContentType()).Returns(MockGroupContentFinder.Object); - Transformer = new(MockMigrationPipeline.Object, MockLogger.Object, MockLocalizer.Object); + Transformer = new(MockDestinationFinderFactory.Object, MockLogger.Object, MockLocalizer.Object); } } @@ -69,7 +68,7 @@ public ExecuteAsync() _idMap = new(); MockUserContentFinder - .Setup(f => f.FindDestinationReferenceAsync(It.IsAny(), Cancel)) + .Setup(f => f.FindBySourceIdAsync(It.IsAny(), Cancel)) .ReturnsAsync((Guid id, CancellationToken cancel) => { if (_idMap.TryGetValue(id, out var destinationId)) @@ -79,7 +78,7 @@ public ExecuteAsync() }); MockGroupContentFinder - .Setup(f => f.FindDestinationReferenceAsync(It.IsAny(), Cancel)) + .Setup(f => f.FindBySourceIdAsync(It.IsAny(), Cancel)) .ReturnsAsync((Guid id, CancellationToken cancel) => { if (_idMap.TryGetValue(id, out var destinationId)) @@ -119,12 +118,12 @@ public async Task Transforms() if (granteeCapability.GranteeType is GranteeType.User) { MockUserContentFinder - .Verify(f => f.FindDestinationReferenceAsync(sourceId, Cancel), Times.Once); + .Verify(f => f.FindBySourceIdAsync(sourceId, Cancel), Times.Once); } else { MockGroupContentFinder - .Verify(f => f.FindDestinationReferenceAsync(sourceId, Cancel), Times.Once); + .Verify(f => f.FindBySourceIdAsync(sourceId, Cancel), Times.Once); } } } @@ -158,12 +157,12 @@ public async Task Resolves_duplicates() if (granteeCapability.GranteeType is GranteeType.User) { MockUserContentFinder - .Verify(f => f.FindDestinationReferenceAsync(sourceId, Cancel), Times.Once); + .Verify(f => f.FindBySourceIdAsync(sourceId, Cancel), Times.Once); } else { MockGroupContentFinder - .Verify(f => f.FindDestinationReferenceAsync(sourceId, Cancel), Times.Once); + .Verify(f => f.FindBySourceIdAsync(sourceId, Cancel), Times.Once); } } } @@ -212,12 +211,12 @@ public async Task Resolves_conflicts() if (granteeCapability.GranteeType is GranteeType.User) { MockUserContentFinder - .Verify(f => f.FindDestinationReferenceAsync(sourceId, Cancel), Times.Once); + .Verify(f => f.FindBySourceIdAsync(sourceId, Cancel), Times.Once); } else { MockGroupContentFinder - .Verify(f => f.FindDestinationReferenceAsync(sourceId, Cancel), Times.Once); + .Verify(f => f.FindBySourceIdAsync(sourceId, Cancel), Times.Once); } } diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/TableauServerConnectionUrlTransformerTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/TableauServerConnectionUrlTransformerTests.cs index 061a4a5..77fec5e 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/TableauServerConnectionUrlTransformerTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/Default/TableauServerConnectionUrlTransformerTests.cs @@ -39,9 +39,12 @@ public class TableauServerConnectionUrlTransformerTests { public class TestTableauServerConnectionUrlTransformer : TableauServerConnectionUrlTransformer { - public TestTableauServerConnectionUrlTransformer(IMigration migration, - ILogger logger, ISharedResourcesLocalizer localizer) - : base(migration, logger, localizer) + public TestTableauServerConnectionUrlTransformer( + IMigration migration, + IDestinationContentReferenceFinderFactory destinationFinderFactory, + ILogger logger, + ISharedResourcesLocalizer localizer) + : base(migration, destinationFinderFactory, logger, localizer) { } public bool PublicNeedsXmlTransforming(IPublishableWorkbook ctx) @@ -50,7 +53,8 @@ public bool PublicNeedsXmlTransforming(IPublishableWorkbook ctx) public class TableauServerConnectionUrlTransformerTest : AutoFixtureTestBase { - protected readonly Mock> MockDataSourceFinder; + protected readonly Mock> MockDataSourceFinder; + protected readonly Mock MockDestinationFinderFactory; protected readonly Mock> MockLog; protected readonly MockSharedResourcesLocalizer MockLocalizer; @@ -67,14 +71,14 @@ protected virtual IMigrationPlanEndpointConfiguration GetDestinationEndpointConf public TableauServerConnectionUrlTransformerTest() { - MockDataSourceFinder = Freeze>>(); + MockDataSourceFinder = Freeze>>(); MockLog = Freeze>>(); MockLocalizer = Freeze(); MockWorkbook = Create>(); DataSourceReferencesBySourceContentUrl = new(); - MockDataSourceFinder.Setup(x => x.FindDestinationReferenceAsync(It.IsAny(), Cancel)) + MockDataSourceFinder.Setup(x => x.FindBySourceContentUrlAsync(It.IsAny(), Cancel)) .ReturnsAsync((string contentUrl, CancellationToken cancel) => { if (DataSourceReferencesBySourceContentUrl.TryGetValue(contentUrl, out var val)) @@ -85,6 +89,10 @@ public TableauServerConnectionUrlTransformerTest() return null; }); + MockDestinationFinderFactory = Freeze>(); + MockDestinationFinderFactory.Setup(x => x.ForDestinationContentType()) + .Returns(MockDataSourceFinder.Object); + DestinationConfig = Create(); MockDestinationApiEndpointConfig = Freeze>(); @@ -93,8 +101,6 @@ public TableauServerConnectionUrlTransformerTest() var mockMigration = Freeze>(); mockMigration.Setup(x => x.Plan.Destination).Returns(GetDestinationEndpointConfig); - mockMigration.Setup(x => x.Pipeline.CreateDestinationFinder()) - .Returns(MockDataSourceFinder.Object); Transformer = Create(); } @@ -158,7 +164,7 @@ public async Task UpdatesConnectionAndRepoLocationAsync() var xml = new XDocument(wb); //Transform - await Transformer.ExecuteAsync(MockWorkbook.Object, xml, Cancel); + await Transformer.TransformAsync(MockWorkbook.Object, xml, Cancel); //Assert Assert.Equal(pdsRef1.ContentUrl, dsRepo.Attribute("id")!.Value); @@ -196,7 +202,7 @@ public async Task UpdatesAllDataSourcesAsync() var xml = new XDocument(wb); //Transform - await Transformer.ExecuteAsync(MockWorkbook.Object, xml, Cancel); + await Transformer.TransformAsync(MockWorkbook.Object, xml, Cancel); //Assert Assert.Equal(pdsRef1.ContentUrl, ds1Repo.Attribute("id")!.Value); @@ -228,7 +234,7 @@ public async Task OnlyUpdatesTableauServerConnectionsAsync() var xml = new XDocument(wb); //Transform - await Transformer.ExecuteAsync(MockWorkbook.Object, xml, Cancel); + await Transformer.TransformAsync(MockWorkbook.Object, xml, Cancel); //Assert Assert.Equal("http", dsConn.Attribute("channel")!.Value); @@ -264,10 +270,10 @@ public async Task WarnsMissingReferenceSingleTimePerWorkbookAndContentUrl() var xml = new XDocument(wb); //Transform - await Transformer.ExecuteAsync(MockWorkbook.Object, xml, Cancel); + await Transformer.TransformAsync(MockWorkbook.Object, xml, Cancel); var mockWorkbook2 = Create>(); - await Transformer.ExecuteAsync(mockWorkbook2.Object, xml, Cancel); + await Transformer.TransformAsync(mockWorkbook2.Object, xml, Cancel); //Assert MockLog.VerifyWarnings(Times.Exactly(4)); diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/IXmlContentTransformerTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/IXmlContentTransformerTests.cs index 1fb7111..5ded610 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/IXmlContentTransformerTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/IXmlContentTransformerTests.cs @@ -32,7 +32,7 @@ public class ExecuteAsync : AutoFixtureTestBase { public class TestImplementation : IXmlContentTransformer { - public virtual async Task ExecuteAsync(TestFileContentType ctx, XDocument xml, CancellationToken cancel) + public virtual async Task TransformAsync(TestFileContentType ctx, XDocument xml, CancellationToken cancel) { await Task.CompletedTask; } @@ -75,7 +75,7 @@ public async Task GetsOrReadsXmlAsync() mockFile.Verify(x => x.GetXmlStreamAsync(Cancel), Times.Once); mockXmlStream.Verify(x => x.GetXmlAsync(Cancel), Times.Once); - mockTransformer.Verify(x => x.ExecuteAsync(ctx, xml, Cancel), Times.Once); + mockTransformer.Verify(x => x.TransformAsync(ctx, xml, Cancel), Times.Once); } } } diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/XmlContentTransformerBaseTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/XmlContentTransformerBaseTests.cs index 7685bda..793cca0 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/XmlContentTransformerBaseTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Hooks/Transformers/XmlContentTransformerBaseTests.cs @@ -60,7 +60,7 @@ public class TestXmlTransformer : XmlContentTransformerBase protected override bool NeedsXmlTransforming(TestFileContentType ctx) => NeedsXmlTransformingFilter?.Invoke(ctx) ?? base.NeedsXmlTransforming(ctx); - public override Task ExecuteAsync(TestFileContentType ctx, XDocument xml, CancellationToken cancel) + public override Task TransformAsync(TestFileContentType ctx, XDocument xml, CancellationToken cancel) { TransformXml?.Invoke(ctx, xml); return Task.CompletedTask; @@ -89,9 +89,9 @@ public async Task CanFilterItemsToTransformAsync() #endregion - #region - ExecuteAsync - + #region - TransformAsync - - public class ExecuteAsync : XmlContentTransformerBaseTest + public class TransformAsync : XmlContentTransformerBaseTest { public class TestOverwriteExecuteXmlTransformer : XmlContentTransformerBase, IMigrationHook @@ -101,7 +101,7 @@ protected override bool NeedsXmlTransforming(TestFileContentType ctx) throw new NotImplementedException(); } - public override Task ExecuteAsync(TestFileContentType ctx, XDocument xml, CancellationToken cancel) + public override Task TransformAsync(TestFileContentType ctx, XDocument xml, CancellationToken cancel) { throw new NotImplementedException(); } diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/IServiceCollectionExtensionsTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/IServiceCollectionExtensionsTests.cs index f9d3215..45f98e2 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/IServiceCollectionExtensionsTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/IServiceCollectionExtensionsTests.cs @@ -272,15 +272,15 @@ public async Task RegistersScopedDestinationContentFinderAsync() { await using var scope = await InitializeMigrationScopeAsync(); - AssertService>(scope, ServiceLifetime.Scoped); + AssertService, ManifestDestinationContentReferenceFinder>(scope, ServiceLifetime.Scoped); } [Fact] - public async Task RegistersScopedDestinationContentFinderFactoryAsync() + public async Task RegistersScopedIDestinationContentFinderFactoryAsync() { await using var scope = await InitializeMigrationScopeAsync(); - AssertService(scope, ServiceLifetime.Scoped); + AssertService(scope, ServiceLifetime.Scoped); } [Fact] @@ -296,15 +296,15 @@ public async Task RegistersScopedSourceContentFinderAsync() { await using var scope = await InitializeMigrationScopeAsync(); - AssertService>(scope, ServiceLifetime.Scoped); + AssertService, ManifestSourceContentReferenceFinder>(scope, ServiceLifetime.Scoped); } [Fact] - public async Task RegistersScopedSourceContentFinderFactoryAsync() + public async Task RegistersScopedISourceContentFinderFactoryAsync() { await using var scope = await InitializeMigrationScopeAsync(); - AssertService(scope, ServiceLifetime.Scoped); + AssertService(scope, ServiceLifetime.Scoped); } [Fact] diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Pipelines/MigrationPipelineBaseTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Pipelines/MigrationPipelineBaseTests.cs index 3f2be32..0e1e6f8 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Pipelines/MigrationPipelineBaseTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Pipelines/MigrationPipelineBaseTests.cs @@ -172,22 +172,6 @@ public void CreatesSpecializedProjectCache() #endregion - #region - CreateDestinationFinder - - - public class CreateDestinationFinder : MigrationPipelineBaseTest - { - [Fact] - public void CreatesLocationFinder() - { - var cache = Pipeline.CreateDestinationFinder(); - - Assert.IsType>(cache); - MockServices.Verify(x => x.GetService(typeof(ManifestDestinationContentReferenceFinder)), Times.Once); - } - } - - #endregion - #region - GetDestinationLockedProjectCache - public class GetDestinationLockedProjectCache : MigrationPipelineBaseTest diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Preparation/ContentItemPreparerBaseTests.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Preparation/ContentItemPreparerBaseTests.cs index 2037657..732280a 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Preparation/ContentItemPreparerBaseTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Preparation/ContentItemPreparerBaseTests.cs @@ -21,8 +21,8 @@ using Moq; using Tableau.Migration.Content; using Tableau.Migration.Engine; +using Tableau.Migration.Engine.Endpoints.Search; using Tableau.Migration.Engine.Hooks.Transformers; -using Tableau.Migration.Engine.Pipelines; using Tableau.Migration.Engine.Preparation; using Xunit; @@ -39,8 +39,10 @@ public class TestPreparer : ContentItemPreparerBase> PullAsync(ContentMigrationItem item, CancellationToken cancel) @@ -53,15 +55,19 @@ protected override Task> PullAsync(ContentMigrationItem : TestPreparer where TContent : class, new() { - public TestPreparer(IContentTransformerRunner transformerRunner, IMigrationPipeline pipeline) - : base(transformerRunner, pipeline) + public TestPreparer( + IContentTransformerRunner transformerRunner, + IDestinationContentReferenceFinderFactory destinationFinderFactory) + : base(transformerRunner, destinationFinderFactory) { } } public class TestPreparer : TestPreparer { - public TestPreparer(IContentTransformerRunner transformerRunner, IMigrationPipeline pipeline) - : base(transformerRunner, pipeline) + public TestPreparer( + IContentTransformerRunner transformerRunner, + IDestinationContentReferenceFinderFactory destinationFinderFactory) + : base(transformerRunner, destinationFinderFactory) { } } @@ -155,7 +161,7 @@ public async Task AppliesMappingToContainerContentAsync() var destinationContainerLocation = MockManifestEntry.Object.MappedLocation.Parent(); var destinationProject = Create(); - MockProjectFinder.Setup(x => x.FindDestinationReferenceAsync(sourceParentLocation, Cancel)) + MockProjectFinder.Setup(x => x.FindBySourceLocationAsync(sourceParentLocation, Cancel)) .ReturnsAsync(destinationProject); var result = await preparer.PrepareAsync(item, Cancel); @@ -168,7 +174,7 @@ public async Task AppliesMappingToContainerContentAsync() Assert.Equal(destinationName, publishItem.Name); Assert.Same(destinationProject, publishItem.Container); - MockProjectFinder.Verify(x => x.FindDestinationReferenceAsync(sourceParentLocation, Cancel), Times.Once); + MockProjectFinder.Verify(x => x.FindBySourceLocationAsync(sourceParentLocation, Cancel), Times.Once); } [Fact] @@ -183,7 +189,7 @@ public async Task AppliesNewParentToContainerContentAsync() var destinationContainerLocation = MockManifestEntry.Object.MappedLocation.Parent(); var destinationProject = Create(); - MockProjectFinder.Setup(x => x.FindMappedDestinationReferenceAsync(destinationContainerLocation, Cancel)) + MockProjectFinder.Setup(x => x.FindByMappedLocationAsync(destinationContainerLocation, Cancel)) .ReturnsAsync(destinationProject); var result = await preparer.PrepareAsync(item, Cancel); @@ -196,7 +202,7 @@ public async Task AppliesNewParentToContainerContentAsync() Assert.Equal(destinationName, publishItem.Name); Assert.Same(destinationProject, publishItem.Container); - MockProjectFinder.Verify(x => x.FindMappedDestinationReferenceAsync(destinationContainerLocation, Cancel), Times.Once); + MockProjectFinder.Verify(x => x.FindByMappedLocationAsync(destinationContainerLocation, Cancel), Times.Once); } [Fact] @@ -220,7 +226,7 @@ public async Task AppliesMappingToTopLevelContainerContentAsync() Assert.Equal(destinationName, publishItem.Name); Assert.Null(publishItem.Container); - MockProjectFinder.Verify(x => x.FindDestinationReferenceAsync(It.IsAny(), Cancel), Times.Never); + MockProjectFinder.Verify(x => x.FindBySourceLocationAsync(It.IsAny(), Cancel), Times.Never); } [Fact] diff --git a/tests/Tableau.Migration.Tests/Unit/Engine/Preparation/ContentItemPreparerTestBase.cs b/tests/Tableau.Migration.Tests/Unit/Engine/Preparation/ContentItemPreparerTestBase.cs index 223c399..127662a 100644 --- a/tests/Tableau.Migration.Tests/Unit/Engine/Preparation/ContentItemPreparerTestBase.cs +++ b/tests/Tableau.Migration.Tests/Unit/Engine/Preparation/ContentItemPreparerTestBase.cs @@ -23,7 +23,6 @@ using Tableau.Migration.Engine.Endpoints.Search; using Tableau.Migration.Engine.Hooks.Transformers; using Tableau.Migration.Engine.Manifest; -using Tableau.Migration.Engine.Pipelines; namespace Tableau.Migration.Tests.Unit.Engine.Preparation { @@ -31,7 +30,7 @@ public class ContentItemPreparerTestBase : AutoFixtureTestBase { protected readonly Mock MockTransformerRunner; protected readonly Mock MockManifestEntry; - protected readonly Mock> MockProjectFinder; + protected readonly Mock> MockProjectFinder; protected readonly Mock MockFileStore; protected readonly ContentMigrationItem Item; @@ -48,12 +47,12 @@ public ContentItemPreparerTestBase() MockManifestEntry = Freeze>(); MockManifestEntry.SetupGet(x => x.MappedLocation).Returns(() => MappedLocation); - MockProjectFinder = Freeze>>(); - MockProjectFinder.Setup(x => x.FindDestinationReferenceAsync(It.IsAny(), Cancel)) + MockProjectFinder = Freeze>>(); + MockProjectFinder.Setup(x => x.FindBySourceLocationAsync(It.IsAny(), Cancel)) .ReturnsAsync((IContentReference?)null); - var mockPipeline = Freeze>(); - mockPipeline.Setup(x => x.CreateDestinationFinder()) + var mockDestinationFinderFactory = Freeze>(); + mockDestinationFinderFactory.Setup(x => x.ForDestinationContentType()) .Returns(MockProjectFinder.Object); MockFileStore = Freeze>(); diff --git a/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/Transformers/ISyncXmlContentTransformerTests.cs b/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/Transformers/ISyncXmlContentTransformerTests.cs index 033739e..9dae865 100644 --- a/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/Transformers/ISyncXmlContentTransformerTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/Interop/Hooks/Transformers/ISyncXmlContentTransformerTests.cs @@ -31,7 +31,7 @@ public class ExecuteAsync : AutoFixtureTestBase { public class TestImplementation : ISyncXmlContentTransformer { - public virtual void Execute(TestFileContentType ctx, XDocument xml) { } + public virtual void Transform(TestFileContentType ctx, XDocument xml) { } public virtual bool NeedsXmlTransforming(TestFileContentType ctx) => true; } @@ -57,7 +57,7 @@ public async Task CallsExecuteAsync() mockFile.Verify(x => x.GetXmlStreamAsync(Cancel), Times.Once); mockXmlStream.Verify(x => x.GetXmlAsync(Cancel), Times.Once); - mockTransformer.Verify(x => x.Execute(ctx, xml), Times.Once); + mockTransformer.Verify(x => x.Transform(ctx, xml), Times.Once); } } } diff --git a/tests/Tableau.Migration.Tests/Unit/Interop/IServiceCollectionExtensionsTests.cs b/tests/Tableau.Migration.Tests/Unit/Interop/IServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..b7ad4ac --- /dev/null +++ b/tests/Tableau.Migration.Tests/Unit/Interop/IServiceCollectionExtensionsTests.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2024, Salesforce, Inc. +// SPDX-License-Identifier: Apache-2 +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Tableau.Migration.Interop; +using Tableau.Migration.Interop.Logging; + +namespace Tableau.Migration.Tests.Unit.Interop +{ + public class IServiceCollectionExtensionsTests + { + public class AddPythonSupport : IServiceCollectionExtensionsTestBase + { + class PythonLogger : NonGenericLoggerBase + { + public override bool IsEnabled(LogLevel logLevel) => true; + + public override void Log( + LogLevel logLevel, + EventId eventId, + string state, + Exception? exception, + string message) + { } + } + + PythonLogger GetLogger(string name) + => new PythonLogger(); + + protected override void ConfigureServices(IServiceCollection services) + { + services.AddPythonSupport(GetLogger); + } + } + } +} diff --git a/tests/Tableau.Migration.Tests/Unit/SdkUserAgentTests.cs b/tests/Tableau.Migration.Tests/Unit/SdkUserAgentTests.cs index beb0408..a2e3a10 100644 --- a/tests/Tableau.Migration.Tests/Unit/SdkUserAgentTests.cs +++ b/tests/Tableau.Migration.Tests/Unit/SdkUserAgentTests.cs @@ -51,7 +51,7 @@ public void DefaultValues() [Fact] public void PythonValues() { - var mockLoggerFactory = new Mock>(); + var mockLoggerFactory = new Mock>(); _servicesCollection.AddPythonSupport(mockLoggerFactory.Object); _services = _servicesCollection.BuildServiceProvider();