Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal class PythonMigrationAssistantViewModel : NotificationObject
private readonly Version dynamoVersion;
private PythonNode PythonNode;

private readonly Func<string, string> codeConverter;
private IDiffViewViewModel currentViewModel;
private SideBySideDiffModel diffModel;

Expand All @@ -43,27 +44,47 @@ public IDiffViewViewModel CurrentViewModel
set { this.currentViewModel = value; RaisePropertyChanged(nameof(this.CurrentViewModel)); }
}

public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel workspace, IPathManager pathManager, Version dynamoVersion)
/// <summary>
/// Initializes the view model, runs the 2-to-3 code migration, and prepares the diff model.
/// If migration fails the view model is placed in an error state rather than propagating the exception.
/// </summary>
/// <param name="pythonNode">The Python node whose script will be migrated.</param>
/// <param name="workspace">The workspace that contains the node.</param>
/// <param name="pathManager">Provides backup and application paths.</param>
/// <param name="dynamoVersion">The running Dynamo version, used to scope disclaimer-dismiss files.</param>
/// <param name="codeConverter">
/// Optional override for the migration function. Defaults to <see cref="ScriptMigrator.MigrateCode"/>.
/// Intended for unit testing only.
/// </param>
public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel workspace, IPathManager pathManager, Version dynamoVersion, Func<string, string> codeConverter = null)
{
PythonNode = pythonNode;
OldCode = pythonNode.Script;

this.workspace = workspace;
backupDirectory = pathManager.BackupDirectory;
this.dynamoVersion = dynamoVersion;
this.codeConverter = codeConverter;

try
{
MigrateCode();
}
catch (PythonException)
{
var sidebyside = new SideBySideDiffBuilder();
diffModel = sidebyside.BuildDiffModel(OldCode, OldCode, false);

SetSideBySideViewModel();

CurrentViewModel.DiffState = State.Error;
// Python script error during migration (e.g. syntax the 2to3 tool cannot parse).
SetMigrationErrorState();
return;
}
catch (Exception ex) when (
ex is TypeLoadException ||
ex is MissingMethodException ||
ex is FileLoadException ||
ex is BadImageFormatException)
{
// Python.Runtime assembly at runtime is incompatible (e.g. pythonnet 2.x instead of 3.x).
// Types or members such as PyModule may be absent in older assemblies.
SetMigrationErrorState();
return;
}
SetSideBySideViewModel();
Expand All @@ -74,12 +95,21 @@ public PythonMigrationAssistantViewModel(PythonNode pythonNode, WorkspaceModel w

private void MigrateCode()
{
NewCode = ScriptMigrator.MigrateCode(OldCode);
var converter = codeConverter ?? ScriptMigrator.MigrateCode;
NewCode = converter(OldCode);

var sidebyside = new SideBySideDiffBuilder();
diffModel = sidebyside.BuildDiffModel(OldCode, NewCode, false);
}

private void SetMigrationErrorState()
{
var sidebyside = new SideBySideDiffBuilder();
diffModel = sidebyside.BuildDiffModel(OldCode, OldCode, false);
SetSideBySideViewModel();
CurrentViewModel.DiffState = State.Error;
}

/// <summary>
/// Replaces the code in the Python node with the code changes made by the Migration Assistant.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System;
using System.IO;
using Dynamo;
using Dynamo.Graph.Workspaces;
using Dynamo.PythonMigration.Differ;
using Dynamo.PythonMigration.MigrationAssistant;
using NUnit.Framework;
using PythonNodeModels;

namespace DynamoPythonTests
{
[TestFixture]
public class PythonMigrationAssistantViewModelTests : DynamoModelTestBase
{
/// <summary>
/// Regression test: when Python.Runtime is an incompatible version (e.g. pythonnet 2.x loaded
/// instead of 3.x), Py.CreateScope() throws TypeLoadException because PyModule does not exist
/// in that assembly. The ViewModel must catch this and show an error state rather than crashing.
/// </summary>
[Test]
public void WhenMigrationThrowsTypeLoadExceptionViewModelShowsErrorState()
{
// Arrange
var pyNode = new PythonNode();
var workspace = CurrentDynamoModel.CurrentWorkspace as WorkspaceModel;
var pathManager = CurrentDynamoModel.PathManager;

Func<string, string> brokenMigrator = _ =>
throw new TypeLoadException("Could not load type 'Python.Runtime.PyModule' from assembly 'Python.Runtime, Version=2.5.2.12086'");

// Act
PythonMigrationAssistantViewModel viewModel = null;
Assert.DoesNotThrow(() =>
{
viewModel = new PythonMigrationAssistantViewModel(
pyNode, workspace, pathManager, new Version(3, 0), brokenMigrator);
});

// Assert
Assert.IsNotNull(viewModel);
Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState);
}

/// <summary>
/// Verifies that a MissingMethodException (e.g. missing member in incompatible Python.Runtime)
/// during migration is handled gracefully and shows an error state.
/// </summary>
[Test]
public void WhenMigrationThrowsMissingMethodExceptionViewModelShowsErrorState()
{
var pyNode = new PythonNode();
var workspace = CurrentDynamoModel.CurrentWorkspace as WorkspaceModel;
var pathManager = CurrentDynamoModel.PathManager;

Func<string, string> brokenMigrator = _ =>
throw new MissingMethodException("Python.Runtime.Py", "CreateScope");

PythonMigrationAssistantViewModel viewModel = null;
Assert.DoesNotThrow(() =>
{
viewModel = new PythonMigrationAssistantViewModel(
pyNode, workspace, pathManager, new Version(3, 0), brokenMigrator);
});

Assert.IsNotNull(viewModel);
Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState);
}

/// <summary>
/// Verifies that a FileLoadException (e.g. assembly version conflict) during migration
/// is handled gracefully and shows an error state.
/// </summary>
[Test]
public void WhenMigrationThrowsFileLoadExceptionViewModelShowsErrorState()
{
var pyNode = new PythonNode();
var workspace = CurrentDynamoModel.CurrentWorkspace as WorkspaceModel;
var pathManager = CurrentDynamoModel.PathManager;

Func<string, string> brokenMigrator = _ =>
throw new FileLoadException("Could not load file or assembly 'Python.Runtime'");

PythonMigrationAssistantViewModel viewModel = null;
Assert.DoesNotThrow(() =>
{
viewModel = new PythonMigrationAssistantViewModel(
pyNode, workspace, pathManager, new Version(3, 0), brokenMigrator);
});

Assert.IsNotNull(viewModel);
Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState);
}

/// <summary>
/// Verifies that a BadImageFormatException (e.g. 32/64-bit or .NET target mismatch in
/// Python.Runtime) during migration is handled gracefully and shows an error state.
/// </summary>
[Test]
public void WhenMigrationThrowsBadImageFormatExceptionViewModelShowsErrorState()
{
var pyNode = new PythonNode();
var workspace = CurrentDynamoModel.CurrentWorkspace as WorkspaceModel;
var pathManager = CurrentDynamoModel.PathManager;

Func<string, string> brokenMigrator = _ =>
throw new BadImageFormatException("The format of the file 'Python.Runtime' is invalid.");

PythonMigrationAssistantViewModel viewModel = null;
Assert.DoesNotThrow(() =>
{
viewModel = new PythonMigrationAssistantViewModel(
pyNode, workspace, pathManager, new Version(3, 0), brokenMigrator);
});

Assert.IsNotNull(viewModel);
Assert.AreEqual(State.Error, viewModel.CurrentViewModel.DiffState);
}
}
}
Loading