Skip to content

Commit

Permalink
Allow dynamic transforms to NOOP
Browse files Browse the repository at this point in the history
Now users can define Starlark transformations which can report NOOPs.
As with the NOOPs reported by the build-in Java transformations, they
will cause a workflow to fail unless ignored via `--ignore-noop`  or
wrapped in a `core.transform(..., noop_behavior='IGNORE_NOOP')`.

BUG=194387788
PiperOrigin-RevId: 387420523
Change-Id: Ie9007975dfbc1b21833e5468216a2be9256c2b8d
  • Loading branch information
martyrubin authored and copybara-github committed Jul 28, 2021
1 parent 7af0df7 commit b37eda8
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 9 deletions.
70 changes: 70 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
- [PathAttributes](#pathattributes)
- [SetReviewInput](#setreviewinput)
- [transformation](#transformation)
- [transformation_status](#transformation_status)
- [TransformWork](#transformwork)
- [ctx.add_label](#ctxadd_label)
- [ctx.add_or_replace_label](#ctxadd_or_replace_label)
Expand All @@ -167,6 +168,7 @@
- [ctx.find_all_labels](#ctxfind_all_labels)
- [ctx.find_label](#ctxfind_label)
- [ctx.new_path](#ctxnew_path)
- [ctx.noop](#ctxnoop)
- [ctx.now_as_string](#ctxnow_as_string)
- [ctx.origin_api](#ctxorigin_api)
- [ctx.read_path](#ctxread_path)
Expand All @@ -176,6 +178,7 @@
- [ctx.set_author](#ctxset_author)
- [ctx.set_executable](#ctxset_executable)
- [ctx.set_message](#ctxset_message)
- [ctx.success](#ctxsuccess)
- [ctx.write_path](#ctxwrite_path)


Expand Down Expand Up @@ -3906,6 +3909,20 @@ A single operation which modifies the source checked out from the origin, prior
## transformation_status
The status of a Transformation that was just run. Either a 'success' or a 'no-op'.
#### Fields:
Name | Description
---- | -----------
is_noop | Whether this status has the value NO-OP.
is_success | Whether this status has the value SUCCESS.
## TransformWork
Data about the set of changes that are being migrated. It includes information about changes like: the author to be used for commit, change message, etc. You receive a TransformWork object as an argument when defining a <a href='#core.dynamic_transform'><code>dynamic transform</code></a>.
Expand Down Expand Up @@ -4039,6 +4056,35 @@ Parameter | Description
--------- | -----------
path | `string`<br><p>The string representing the path, relative to the checkout root directory</p>
<a id="ctx.noop" aria-hidden="true"></a>
### ctx.noop
The status returned by a no-op Transformation
`transformation_status ctx.noop(message)`
#### Parameters:
Parameter | Description
--------- | -----------
message | `string`<br><p></p>
#### Example:
##### Define a dynamic transformation:
Create a custom transformation which fails.
```python
def my_transform(ctx):
# do some stuff
return ctx.noop('Error! The transform didn\'t do anything.')
```
<a id="ctx.now_as_string" aria-hidden="true"></a>
### ctx.now_as_string
Expand Down Expand Up @@ -4164,6 +4210,30 @@ Parameter | Description
--------- | -----------
message | `string`<br><p></p>
<a id="ctx.success" aria-hidden="true"></a>
### ctx.success
The status returned by a successful Transformation
`transformation_status ctx.success()`
#### Example:
##### Define a dynamic transformation:
Create a custom transformation which is successful.
```python
def my_transform(ctx):
# do some stuff
return ctx.success()
```
For compatibility reasons, returning nothing is the same as returning success.
<a id="ctx.write_path" aria-hidden="true"></a>
### ctx.write_path
Expand Down
29 changes: 27 additions & 2 deletions java/com/google/copybara/TransformWork.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.copybara.authoring.Author;
import com.google.copybara.doc.annotations.DocSignaturePrefix;
import com.google.copybara.doc.annotations.Example;
import com.google.copybara.exception.RepoException;
import com.google.copybara.exception.ValidationException;
import com.google.copybara.treestate.TreeState;
Expand Down Expand Up @@ -238,15 +239,39 @@ public Object run(Object runnable)
// Can never trust the cache when inside a dynamic transform. This makes the cache
// more-or-less useless here.
this.treeState.clearCache();
((Transformation) runnable).transform(this);
return Starlark.NONE;
return ((Transformation) runnable).transform(this);
}

throw Starlark.errorf(
"Only globs or transforms can be run, but '%s' is of type %s",
runnable, runnable.getClass());
}

@StarlarkMethod(name = "success", doc = "The status returned by a successful Transformation")
@Example(
title = "Define a dynamic transformation",
before = "Create a custom transformation which is successful.",
code = "def my_transform(ctx):\n" + " # do some stuff\n" + " return ctx.success()",
after = "For compatibility reasons, returning nothing is the same as returning success.")
public TransformationStatus success() {
return TransformationStatus.success();
}

@StarlarkMethod(
name = "noop",
doc = "The status returned by a no-op Transformation",
parameters = {@Param(name = "message")})
@Example(
title = "Define a dynamic transformation",
before = "Create a custom transformation which fails.",
code =
"def my_transform(ctx):\n"
+ " # do some stuff\n"
+ " return ctx.noop('Error! The transform didn\\'t do anything.')")
public TransformationStatus noop(String message) {
return TransformationStatus.noop(message);
}

@StarlarkMethod(
name = "new_path",
doc = "Create a new path",
Expand Down
22 changes: 18 additions & 4 deletions java/com/google/copybara/TransformationStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@
import com.google.common.base.Preconditions;
import com.google.copybara.exception.VoidOperationException;
import com.google.copybara.util.console.Console;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.StarlarkValue;

/**
* The Status of a Transformation that was just run. Either a 'success' or a 'no-op'.
*/
public final class TransformationStatus {
/** The status of a Transformation that was just run. Either a 'success' or a 'no-op'. */
@SuppressWarnings("unused")
@StarlarkBuiltin(
name = TransformationStatus.STARLARK_TYPE_NAME,
doc = "The status of a Transformation that was just run. Either a 'success' or a 'no-op'.")
public final class TransformationStatus implements StarlarkValue {

public static final String STARLARK_TYPE_NAME = "transformation_status";
private final boolean isSuccess;
private final String message;

Expand All @@ -41,10 +47,18 @@ public static TransformationStatus noop(String message) {
return new TransformationStatus(false, message);
}

@StarlarkMethod(
name = "is_success",
doc = "Whether this status has the value SUCCESS.",
structField = true)
public boolean isSuccess() {
return isSuccess;
}

@StarlarkMethod(
name = "is_noop",
doc = "Whether this status has the value NO-OP.",
structField = true)
public boolean isNoop() {
return !isSuccess;
}
Expand Down
11 changes: 8 additions & 3 deletions java/com/google/copybara/transform/SkylarkTransformation.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,22 @@ public TransformationStatus transform(TransformWork work)
SkylarkConsole skylarkConsole = new SkylarkConsole(work.getConsole());
TransformWork skylarkWork = work.withConsole(skylarkConsole)
.withParams(params);
TransformationStatus status = TransformationStatus.success();
try (Mutability mu = Mutability.create("dynamic_transform")) {
StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
thread.setPrintHandler(printHandler);
Object result =
Starlark.call(
thread, function, ImmutableList.of(skylarkWork), /*kwargs=*/ ImmutableMap.of());
result = result == Starlark.NONE ? TransformationStatus.success() : result;
checkCondition(
result == Starlark.NONE,
"Message transformer functions should not return anything, but '%s' returned: %s",
result instanceof TransformationStatus,
"Dynamic transforms functions should return nothing or objects of type %s, but '%s'"
+ " returned: %s",
TransformationStatus.STARLARK_TYPE_NAME,
function.getName(),
result);
status = (TransformationStatus) result;
} catch (EvalException e) {
if (e.getCause() instanceof EmptyChangeException) {
throw ((EmptyChangeException) e.getCause());
Expand All @@ -96,7 +101,7 @@ public TransformationStatus transform(TransformWork work)

checkCondition(skylarkConsole.getErrorCount() == 0, "%d error(s) while executing %s",
skylarkConsole.getErrorCount(), function.getName());
return TransformationStatus.success();
return status;
}

@Override
Expand Down
133 changes: 133 additions & 0 deletions javatests/com/google/copybara/transform/SkylarkTransformationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright (C) 2016 Google Inc.
*
* 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.
*/

package com.google.copybara.transform;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.jimfs.Jimfs;
import com.google.copybara.TransformWork;
import com.google.copybara.Transformation;
import com.google.copybara.TransformationStatus;
import com.google.copybara.testing.OptionsBuilder;
import com.google.copybara.testing.SkylarkTestExecutor;
import com.google.copybara.testing.TransformWorks;
import com.google.copybara.util.console.testing.TestingConsole;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class SkylarkTransformationTest {

private OptionsBuilder options;
private Path checkoutDir;
private TestingConsole console;
private SkylarkTestExecutor skylark;
private TransformWork transformWork;

@Before
public void setup() throws IOException {
FileSystem fs = Jimfs.newFileSystem();
checkoutDir = fs.getPath("/test-checkoutDir");
Files.createDirectories(checkoutDir);
console = new TestingConsole();
options = new OptionsBuilder().setConsole(console);
skylark = new SkylarkTestExecutor(options);
transformWork = TransformWorks.of(checkoutDir, "testmsg", console);
}

@Test
public void testStarlarkTransform_returnsSuccess() throws Exception {
Transformation t =
skylark.eval(
"t",
""
+ "def foo(ctx):\n"
+ " return ctx.success()\n"
+ "\n"
+ "t = core.dynamic_transform(foo)");

TransformationStatus status = t.transform(transformWork);

assertThat(status.isSuccess()).isTrue();
}

@Test
public void testStarlarkTransform_returnsNoop() throws Exception {
Transformation t =
skylark.eval(
"t",
""
+ "def foo(ctx):\n"
+ " return ctx.noop('Reason for noop.')\n"
+ "\n"
+ "t = core.dynamic_transform(foo)");

TransformationStatus status = t.transform(transformWork);

assertThat(status.isNoop()).isTrue();
assertThat(status.getMessage()).isEqualTo("Reason for noop.");
}

@Test
public void testStarlarkTransform_noReturnValue_isTreatedAsSuccess() throws Exception {
Transformation t =
skylark.eval(
"t",
""
+ "def foo(ctx):\n"
+ " # Do nothing \n"
+ " pass\n"
+ "\n"
+ "t = core.dynamic_transform(foo)");

TransformationStatus status = t.transform(transformWork);

assertThat(status.isSuccess()).isTrue();
}

@Test
public void testStarlarkTransform_returnValueCanBeCasedOn() throws Exception {
Transformation t =
skylark.eval(
"t",
""
+ "def foo(ctx):\n"
+ " return ctx.success()\n"
+ "\n"
+ "s = core.dynamic_transform(foo)"
+ "\n"
+ "def bar(ctx):\n"
+ " status = ctx.run(s)\n"
+ " if not status.is_success:\n"
+ " core.fail_with_noop()\n"
+ " if status.is_noop:\n"
+ " core.fail_with_noop()\n"
+ " return status"
+ "\n"
+ "t = core.dynamic_transform(foo)");

TransformationStatus status = t.transform(transformWork);

assertThat(status.isSuccess()).isTrue();
}
}

0 comments on commit b37eda8

Please sign in to comment.