/* Copyright 2015 The TensorFlow Authors. All Rights Reserved.

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.
==============================================================================*/

#include "tensorflow/c/c_api.h"

#include <algorithm>
#include <cstddef>
#include <iterator>
#include <memory>
#include <vector>

#include "tensorflow/c/c_test_util.h"
#include "tensorflow/c/tf_status.h"
#include "tensorflow/cc/saved_model/signature_constants.h"
#include "tensorflow/cc/saved_model/tag_constants.h"
#include "tensorflow/core/example/example.pb.h"
#include "tensorflow/core/example/feature.pb.h"
#include "tensorflow/core/framework/api_def.pb.h"
#include "tensorflow/core/framework/common_shape_fns.h"
#include "tensorflow/core/framework/graph.pb_text.h"
#include "tensorflow/core/framework/kernel_def.pb.h"
#include "tensorflow/core/framework/node_def.pb_text.h"
#include "tensorflow/core/framework/node_def_util.h"
#include "tensorflow/core/framework/op.h"
#include "tensorflow/core/framework/op_def.pb.h"
#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/framework/partial_tensor_shape.h"
#include "tensorflow/core/framework/tensor.h"
#include "tensorflow/core/framework/tensor_shape.pb.h"
#include "tensorflow/core/framework/types.pb.h"
#include "tensorflow/core/graph/tensor_id.h"
#include "tensorflow/core/lib/core/error_codes.pb.h"
#include "tensorflow/core/lib/core/status_test_util.h"
#include "tensorflow/core/lib/io/path.h"
#include "tensorflow/core/lib/strings/str_util.h"
#include "tensorflow/core/lib/strings/strcat.h"
#include "tensorflow/core/platform/test.h"
#include "tensorflow/core/protobuf/meta_graph.pb.h"
#include "tensorflow/core/util/equal_graph_def.h"

namespace tensorflow {
TF_Tensor* TF_TensorFromTensor(const Tensor& src, TF_Status* status);
Status TF_TensorToTensor(const TF_Tensor* src, Tensor* dst);

namespace {

static void ExpectHasSubstr(StringPiece s, StringPiece expected) {
  EXPECT_TRUE(absl::StrContains(s, expected))
      << "'" << s << "' does not contain '" << expected << "'";
}

// Returns the GPU device name if there is one (with arbitrary tie breaking if
// there are more than one), or "" otherwise.
string GPUDeviceName(TF_Session* session) {
  std::unique_ptr<TF_Status, decltype(&TF_DeleteStatus)> status(
      TF_NewStatus(), TF_DeleteStatus);
  TF_Status* s = status.get();
  std::unique_ptr<TF_DeviceList, decltype(&TF_DeleteDeviceList)> list(
      TF_SessionListDevices(session, s), TF_DeleteDeviceList);
  TF_DeviceList* device_list = list.get();

  CHECK_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  const int num_devices = TF_DeviceListCount(device_list);
  LOG(INFO) << "There are " << num_devices << " devices.";
  for (int i = 0; i < num_devices; ++i) {
    const char* device_name = TF_DeviceListName(device_list, i, s);
    CHECK_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
    const char* device_type = TF_DeviceListType(device_list, i, s);
    CHECK_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
    LOG(INFO) << "Device " << i << " has name " << device_name << ", type "
              << device_type;
    if (string(device_type) == DEVICE_GPU) {
      return device_name;
    }
  }
  // No GPU device found.
  return "";
}

string GPUDeviceName() {
  std::unique_ptr<TF_Status, decltype(&TF_DeleteStatus)> status(
      TF_NewStatus(), TF_DeleteStatus);
  TF_Status* s = status.get();
  std::unique_ptr<TF_Graph, decltype(&TF_DeleteGraph)> graph(TF_NewGraph(),
                                                             TF_DeleteGraph);

  TF_SessionOptions* opts = TF_NewSessionOptions();
  TF_Session* sess = TF_NewSession(graph.get(), opts, s);
  TF_DeleteSessionOptions(opts);

  const string gpu_device_name = GPUDeviceName(sess);
  TF_DeleteSession(sess, s);
  CHECK_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  return gpu_device_name;
}

TEST(CAPI, Version) { EXPECT_STRNE("", TF_Version()); }

TEST(CAPI, Status) {
  TF_Status* s = TF_NewStatus();
  EXPECT_EQ(TF_OK, TF_GetCode(s));
  EXPECT_EQ(string(), TF_Message(s));
  TF_SetStatus(s, TF_CANCELLED, "cancel");
  EXPECT_EQ(TF_CANCELLED, TF_GetCode(s));
  EXPECT_EQ(string("cancel"), TF_Message(s));
  TF_DeleteStatus(s);
}

void Deallocator(void* data, size_t, void* arg) {
  tensorflow::cpu_allocator()->DeallocateRaw(data);
  *reinterpret_cast<bool*>(arg) = true;
}

TEST(CAPI, Tensor) {
  const int num_bytes = 6 * sizeof(float);
  float* values =
      reinterpret_cast<float*>(tensorflow::cpu_allocator()->AllocateRaw(
          EIGEN_MAX_ALIGN_BYTES, num_bytes));
  int64_t dims[] = {2, 3};
  bool deallocator_called = false;
  TF_Tensor* t = TF_NewTensor(TF_FLOAT, dims, 2, values, num_bytes,
                              &Deallocator, &deallocator_called);
  EXPECT_FALSE(deallocator_called);
  EXPECT_EQ(TF_FLOAT, TF_TensorType(t));
  EXPECT_EQ(2, TF_NumDims(t));
  EXPECT_EQ(dims[0], TF_Dim(t, 0));
  EXPECT_EQ(dims[1], TF_Dim(t, 1));
  EXPECT_EQ(num_bytes, TF_TensorByteSize(t));
  EXPECT_EQ(static_cast<void*>(values), TF_TensorData(t));
  TF_DeleteTensor(t);
  EXPECT_TRUE(deallocator_called);
}

void NoOpDeallocator(void* data, size_t, void*) {}

TEST(CAPI, MalformedTensor) {
  // See https://github.com/tensorflow/tensorflow/issues/7394
  // num_dims = 0 implies a scalar, so should be backed by at least 4 bytes of
  // data.
  TF_Tensor* t =
      TF_NewTensor(TF_FLOAT, nullptr, 0, nullptr, 0, &NoOpDeallocator, nullptr);
  ASSERT_TRUE(t == nullptr);
}

TEST(CAPI, AllocateTensor) {
  const int num_bytes = 6 * sizeof(float);
  int64_t dims[] = {2, 3};
  TF_Tensor* t = TF_AllocateTensor(TF_FLOAT, dims, 2, num_bytes);
  EXPECT_EQ(TF_FLOAT, TF_TensorType(t));
  EXPECT_EQ(2, TF_NumDims(t));
  EXPECT_EQ(dims[0], TF_Dim(t, 0));
  EXPECT_EQ(dims[1], TF_Dim(t, 1));
  EXPECT_EQ(num_bytes, TF_TensorByteSize(t));
  EXPECT_EQ(6, TF_TensorElementCount(t));
  TF_DeleteTensor(t);
}

TEST(CAPI, MaybeMove) {
  const int num_bytes = 6 * sizeof(float);
  float* values =
      reinterpret_cast<float*>(tensorflow::cpu_allocator()->AllocateRaw(
          EIGEN_MAX_ALIGN_BYTES, num_bytes));
  int64_t dims[] = {2, 3};
  bool deallocator_called = false;
  TF_Tensor* t = TF_NewTensor(TF_FLOAT, dims, 2, values, num_bytes,
                              &Deallocator, &deallocator_called);

  TF_Tensor* o = TF_TensorMaybeMove(t);
  ASSERT_TRUE(o == nullptr);  // It is unsafe to move memory TF might not own.
  TF_DeleteTensor(t);
  EXPECT_TRUE(deallocator_called);
}

TEST(CAPI, LibraryLoadFunctions) {
  // TODO(b/73318067): Fix linking for the GPU test generated by the
  // tf_cuda_cc_test() bazel rule and remove the next line.
  if (!GPUDeviceName().empty()) return;

#if !defined(TENSORFLOW_NO_SHARED_OBJECTS)
  {
    // Load the library.
    TF_Status* status = TF_NewStatus();
    TF_Library* lib =
        TF_LoadLibrary("tensorflow/c/test_op1.so", status);
    TF_Code code = TF_GetCode(status);
    string status_msg(TF_Message(status));
    TF_DeleteStatus(status);
    ASSERT_EQ(TF_OK, code) << status_msg;

    // Test op list.
    TF_Buffer op_list_buf = TF_GetOpList(lib);
    tensorflow::OpList op_list;
    EXPECT_TRUE(op_list.ParseFromArray(op_list_buf.data, op_list_buf.length));
    ASSERT_EQ(op_list.op_size(), 1);
    EXPECT_EQ("TestCApi1", op_list.op(0).name());
    TF_DeleteLibraryHandle(lib);
  }
#endif  // !defined(TENSORFLOW_NO_SHARED_OBJECTS)
  {
    TF_Buffer* op_list_buffer = TF_GetAllOpList();
    tensorflow::OpList op_list;
    op_list.ParseFromArray(op_list_buffer->data, op_list_buffer->length);
    ASSERT_GE(op_list.op_size(), 1);
    typedef tensorflow::protobuf::RepeatedPtrField<tensorflow::OpDef> OpDefs;
    const OpDefs& ops = op_list.op();
    bool found = std::find_if(ops.begin(), ops.end(),
                              [](const tensorflow::OpDef& op_def) {
                                return op_def.name() == "TestCApi";
                              }) != ops.end();
    EXPECT_TRUE(found);
    TF_DeleteBuffer(op_list_buffer);
  }
}

void TestEncodeDecode(int line, const std::vector<string>& data) {
  const tensorflow::int64 n = data.size();
  TF_Status* status = TF_NewStatus();
  for (const std::vector<tensorflow::int64>& dims :
       std::vector<std::vector<tensorflow::int64>>{
           {n}, {1, n}, {n, 1}, {n / 2, 2}}) {
    // Create C++ Tensor
    Tensor src(tensorflow::DT_STRING, TensorShape(dims));
    for (tensorflow::int64 i = 0; i < src.NumElements(); ++i) {
      src.flat<tstring>()(i) = data[i];
    }
    TF_Tensor* dst = TF_TensorFromTensor(src, status);
    ASSERT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);

    // Convert back to a C++ Tensor and ensure we get expected output.
    Tensor output;
    ASSERT_EQ(Status::OK(), TF_TensorToTensor(dst, &output)) << line;
    ASSERT_EQ(src.NumElements(), output.NumElements()) << line;
    for (tensorflow::int64 i = 0; i < src.NumElements(); ++i) {
      ASSERT_EQ(data[i], output.flat<tstring>()(i)) << line;
    }

    TF_DeleteTensor(dst);
  }
  TF_DeleteStatus(status);
}

TEST(CAPI, TensorEncodeDecodeStrings) {
  TestEncodeDecode(__LINE__, {});
  TestEncodeDecode(__LINE__, {"hello"});
  TestEncodeDecode(__LINE__,
                   {"the", "quick", "brown", "fox", "jumped", "over"});

  string big(1000, 'a');
  TestEncodeDecode(__LINE__, {"small", big, "small2"});
}

TEST(CAPI, SessionOptions) {
  TF_SessionOptions* opt = TF_NewSessionOptions();
  TF_DeleteSessionOptions(opt);
}

TEST(CAPI, DeprecatedSession) {
  TF_Status* s = TF_NewStatus();
  TF_SessionOptions* opt = TF_NewSessionOptions();
  TF_DeprecatedSession* session = TF_NewDeprecatedSession(opt, s);
  TF_DeleteSessionOptions(opt);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Buffer* run_options = TF_NewBufferFromString("", 0);
  TF_Buffer* run_metadata = TF_NewBuffer();
  TF_Run(session, run_options, nullptr, nullptr, 0, nullptr, nullptr, 0,
         nullptr, 0, run_metadata, s);
  EXPECT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ("Session was not created with a graph before Run()!",
            string(TF_Message(s)));
  TF_DeleteBuffer(run_metadata);
  TF_DeleteBuffer(run_options);

  TF_DeleteDeprecatedSession(session, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_DeleteStatus(s);
}

TEST(CAPI, DataTypeEnum) {
  EXPECT_EQ(TF_FLOAT, static_cast<TF_DataType>(tensorflow::DT_FLOAT));
  EXPECT_EQ(TF_DOUBLE, static_cast<TF_DataType>(tensorflow::DT_DOUBLE));
  EXPECT_EQ(TF_INT32, static_cast<TF_DataType>(tensorflow::DT_INT32));
  EXPECT_EQ(TF_UINT8, static_cast<TF_DataType>(tensorflow::DT_UINT8));
  EXPECT_EQ(TF_INT16, static_cast<TF_DataType>(tensorflow::DT_INT16));
  EXPECT_EQ(TF_INT8, static_cast<TF_DataType>(tensorflow::DT_INT8));
  EXPECT_EQ(TF_STRING, static_cast<TF_DataType>(tensorflow::DT_STRING));
  EXPECT_EQ(TF_COMPLEX64, static_cast<TF_DataType>(tensorflow::DT_COMPLEX64));
  EXPECT_EQ(TF_COMPLEX, TF_COMPLEX64);
  EXPECT_EQ(TF_INT64, static_cast<TF_DataType>(tensorflow::DT_INT64));
  EXPECT_EQ(TF_BOOL, static_cast<TF_DataType>(tensorflow::DT_BOOL));
  EXPECT_EQ(TF_QINT8, static_cast<TF_DataType>(tensorflow::DT_QINT8));
  EXPECT_EQ(TF_QUINT8, static_cast<TF_DataType>(tensorflow::DT_QUINT8));
  EXPECT_EQ(TF_QINT32, static_cast<TF_DataType>(tensorflow::DT_QINT32));
  EXPECT_EQ(TF_BFLOAT16, static_cast<TF_DataType>(tensorflow::DT_BFLOAT16));
  EXPECT_EQ(TF_QINT16, static_cast<TF_DataType>(tensorflow::DT_QINT16));
  EXPECT_EQ(TF_QUINT16, static_cast<TF_DataType>(tensorflow::DT_QUINT16));
  EXPECT_EQ(TF_UINT16, static_cast<TF_DataType>(tensorflow::DT_UINT16));
  EXPECT_EQ(TF_COMPLEX128, static_cast<TF_DataType>(tensorflow::DT_COMPLEX128));
  EXPECT_EQ(TF_HALF, static_cast<TF_DataType>(tensorflow::DT_HALF));
  EXPECT_EQ(TF_DataTypeSize(TF_DOUBLE),
            tensorflow::DataTypeSize(tensorflow::DT_DOUBLE));
  EXPECT_EQ(TF_DataTypeSize(TF_STRING),
            tensorflow::DataTypeSize(tensorflow::DT_STRING));
  // Test with invalid type; should always return 0 as documented
  EXPECT_EQ(TF_DataTypeSize(static_cast<TF_DataType>(0)), 0);
}

TEST(CAPI, StatusEnum) {
  EXPECT_EQ(TF_OK, static_cast<TF_Code>(tensorflow::error::OK));
  EXPECT_EQ(TF_CANCELLED, static_cast<TF_Code>(tensorflow::error::CANCELLED));
  EXPECT_EQ(TF_UNKNOWN, static_cast<TF_Code>(tensorflow::error::UNKNOWN));
  EXPECT_EQ(TF_INVALID_ARGUMENT,
            static_cast<TF_Code>(tensorflow::error::INVALID_ARGUMENT));
  EXPECT_EQ(TF_DEADLINE_EXCEEDED,
            static_cast<TF_Code>(tensorflow::error::DEADLINE_EXCEEDED));
  EXPECT_EQ(TF_NOT_FOUND, static_cast<TF_Code>(tensorflow::error::NOT_FOUND));
  EXPECT_EQ(TF_ALREADY_EXISTS,
            static_cast<TF_Code>(tensorflow::error::ALREADY_EXISTS));
  EXPECT_EQ(TF_PERMISSION_DENIED,
            static_cast<TF_Code>(tensorflow::error::PERMISSION_DENIED));
  EXPECT_EQ(TF_UNAUTHENTICATED,
            static_cast<TF_Code>(tensorflow::error::UNAUTHENTICATED));
  EXPECT_EQ(TF_RESOURCE_EXHAUSTED,
            static_cast<TF_Code>(tensorflow::error::RESOURCE_EXHAUSTED));
  EXPECT_EQ(TF_FAILED_PRECONDITION,
            static_cast<TF_Code>(tensorflow::error::FAILED_PRECONDITION));
  EXPECT_EQ(TF_ABORTED, static_cast<TF_Code>(tensorflow::error::ABORTED));
  EXPECT_EQ(TF_OUT_OF_RANGE,
            static_cast<TF_Code>(tensorflow::error::OUT_OF_RANGE));
  EXPECT_EQ(TF_UNIMPLEMENTED,
            static_cast<TF_Code>(tensorflow::error::UNIMPLEMENTED));
  EXPECT_EQ(TF_INTERNAL, static_cast<TF_Code>(tensorflow::error::INTERNAL));
  EXPECT_EQ(TF_UNAVAILABLE,
            static_cast<TF_Code>(tensorflow::error::UNAVAILABLE));
  EXPECT_EQ(TF_DATA_LOSS, static_cast<TF_Code>(tensorflow::error::DATA_LOSS));
}

TEST(CAPI, GetAllOpList) {
  TF_Buffer* buf = TF_GetAllOpList();
  tensorflow::OpList op_list;
  EXPECT_TRUE(op_list.ParseFromArray(buf->data, buf->length));
  EXPECT_GT(op_list.op_size(), 0);
  TF_DeleteBuffer(buf);
}

TEST(CAPI, SetShape) {
  TF_Status* s = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();

  TF_Operation* feed = Placeholder(graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_Output feed_out_0 = TF_Output{feed, 0};
  int num_dims;

  // Fetch the shape, it should be completely unknown.
  num_dims = TF_GraphGetTensorNumDims(graph, feed_out_0, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(-1, num_dims);

  // Set the shape to be unknown, expect no change.
  TF_GraphSetTensorShape(graph, feed_out_0, /*dims=*/nullptr, -1, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  num_dims = TF_GraphGetTensorNumDims(graph, feed_out_0, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(-1, num_dims);

  // Set the shape to be 2 x Unknown
  int64_t dims[] = {2, -1};
  TF_GraphSetTensorShape(graph, feed_out_0, dims, 2, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Fetch the shape and validate it is 2 by -1.
  num_dims = TF_GraphGetTensorNumDims(graph, feed_out_0, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(2, num_dims);

  // Resize the dimension vector appropriately.
  int64_t returned_dims[2];
  TF_GraphGetTensorShape(graph, feed_out_0, returned_dims, num_dims, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(dims[0], returned_dims[0]);
  EXPECT_EQ(dims[1], returned_dims[1]);

  // Set to a new valid shape: [2, 3]
  dims[1] = 3;
  TF_GraphSetTensorShape(graph, feed_out_0, dims, 2, s);
  EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Fetch and see that the new value is returned.
  TF_GraphGetTensorShape(graph, feed_out_0, returned_dims, num_dims, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(dims[0], returned_dims[0]);
  EXPECT_EQ(dims[1], returned_dims[1]);

  // Try to set 'unknown' with unknown rank on the shape and see that
  // it doesn't change.
  TF_GraphSetTensorShape(graph, feed_out_0, /*dims=*/nullptr, -1, s);
  EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_GraphGetTensorShape(graph, feed_out_0, returned_dims, num_dims, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(2, num_dims);
  EXPECT_EQ(2, returned_dims[0]);
  EXPECT_EQ(3, returned_dims[1]);

  // Try to set 'unknown' with same rank on the shape and see that
  // it doesn't change.
  dims[0] = -1;
  dims[1] = -1;
  TF_GraphSetTensorShape(graph, feed_out_0, dims, 2, s);
  EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  // Fetch and see that the new value is returned.
  TF_GraphGetTensorShape(graph, feed_out_0, returned_dims, num_dims, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(2, num_dims);
  EXPECT_EQ(2, returned_dims[0]);
  EXPECT_EQ(3, returned_dims[1]);

  // Try to fetch a shape with the wrong num_dims
  TF_GraphGetTensorShape(graph, feed_out_0, returned_dims, 5, s);
  EXPECT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s)) << TF_Message(s);

  // Try to set an invalid shape (cannot change 2x3 to a 2x5).
  dims[1] = 5;
  TF_GraphSetTensorShape(graph, feed_out_0, dims, 2, s);
  EXPECT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s)) << TF_Message(s);

  // Test for a scalar.
  TF_Operation* three = ScalarConst(3, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_Output three_out_0 = TF_Output{three, 0};

  num_dims = TF_GraphGetTensorNumDims(graph, three_out_0, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(0, num_dims);
  TF_GraphGetTensorShape(graph, three_out_0, returned_dims, num_dims, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Clean up
  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

TEST(CAPI, Graph) {
  TF_Status* s = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();

  // Make a placeholder operation.
  TF_Operation* feed = Placeholder(graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Test TF_Operation*() query functions.
  EXPECT_EQ(string("feed"), string(TF_OperationName(feed)));
  EXPECT_EQ(string("Placeholder"), string(TF_OperationOpType(feed)));
  EXPECT_EQ(string(""), string(TF_OperationDevice(feed)));
  EXPECT_EQ(1, TF_OperationNumOutputs(feed));
  EXPECT_EQ(TF_INT32, TF_OperationOutputType(TF_Output{feed, 0}));
  EXPECT_EQ(1, TF_OperationOutputListLength(feed, "output", s));
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(0, TF_OperationNumInputs(feed));
  EXPECT_EQ(0, TF_OperationOutputNumConsumers(TF_Output{feed, 0}));
  EXPECT_EQ(0, TF_OperationNumControlInputs(feed));
  EXPECT_EQ(0, TF_OperationNumControlOutputs(feed));

  tensorflow::AttrValue attr_value;
  ASSERT_TRUE(GetAttrValue(feed, "dtype", &attr_value, s)) << TF_Message(s);
  EXPECT_EQ(attr_value.type(), tensorflow::DT_INT32);

  // Test not found errors in TF_Operation*() query functions.
  EXPECT_EQ(-1, TF_OperationOutputListLength(feed, "bogus", s));
  EXPECT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s));

  ASSERT_FALSE(GetAttrValue(feed, "missing", &attr_value, s));
  EXPECT_EQ(string("Operation 'feed' has no attr named 'missing'."),
            string(TF_Message(s)));

  // Make a constant oper with the scalar "3".
  TF_Operation* three = ScalarConst(3, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Add oper.
  TF_Operation* add = Add(feed, three, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Test TF_Operation*() query functions.
  EXPECT_EQ(string("add"), string(TF_OperationName(add)));
  EXPECT_EQ(string("AddN"), string(TF_OperationOpType(add)));
  EXPECT_EQ(string(""), string(TF_OperationDevice(add)));
  EXPECT_EQ(1, TF_OperationNumOutputs(add));
  EXPECT_EQ(TF_INT32, TF_OperationOutputType(TF_Output{add, 0}));
  EXPECT_EQ(1, TF_OperationOutputListLength(add, "sum", s));
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(2, TF_OperationNumInputs(add));
  EXPECT_EQ(2, TF_OperationInputListLength(add, "inputs", s));
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(TF_INT32, TF_OperationInputType(TF_Input{add, 0}));
  EXPECT_EQ(TF_INT32, TF_OperationInputType(TF_Input{add, 1}));
  TF_Output add_in_0 = TF_OperationInput(TF_Input{add, 0});
  EXPECT_EQ(feed, add_in_0.oper);
  EXPECT_EQ(0, add_in_0.index);
  TF_Output add_in_1 = TF_OperationInput(TF_Input{add, 1});
  EXPECT_EQ(three, add_in_1.oper);
  EXPECT_EQ(0, add_in_1.index);
  EXPECT_EQ(0, TF_OperationOutputNumConsumers(TF_Output{add, 0}));
  EXPECT_EQ(0, TF_OperationNumControlInputs(add));
  EXPECT_EQ(0, TF_OperationNumControlOutputs(add));

  ASSERT_TRUE(GetAttrValue(add, "T", &attr_value, s)) << TF_Message(s);
  EXPECT_EQ(attr_value.type(), tensorflow::DT_INT32);
  ASSERT_TRUE(GetAttrValue(add, "N", &attr_value, s)) << TF_Message(s);
  EXPECT_EQ(attr_value.i(), 2);

  // Placeholder oper now has a consumer.
  ASSERT_EQ(1, TF_OperationOutputNumConsumers(TF_Output{feed, 0}));
  TF_Input feed_port;
  EXPECT_EQ(1, TF_OperationOutputConsumers(TF_Output{feed, 0}, &feed_port, 1));
  EXPECT_EQ(add, feed_port.oper);
  EXPECT_EQ(0, feed_port.index);

  // The scalar const oper also has a consumer.
  ASSERT_EQ(1, TF_OperationOutputNumConsumers(TF_Output{three, 0}));
  TF_Input three_port;
  EXPECT_EQ(1,
            TF_OperationOutputConsumers(TF_Output{three, 0}, &three_port, 1));
  EXPECT_EQ(add, three_port.oper);
  EXPECT_EQ(1, three_port.index);

  // Serialize to GraphDef.
  GraphDef graph_def;
  ASSERT_TRUE(GetGraphDef(graph, &graph_def));

  // Validate GraphDef is what we expect.
  bool found_placeholder = false;
  bool found_scalar_const = false;
  bool found_add = false;
  for (const auto& n : graph_def.node()) {
    if (IsPlaceholder(n)) {
      EXPECT_FALSE(found_placeholder);
      found_placeholder = true;
    } else if (IsScalarConst(n, 3)) {
      EXPECT_FALSE(found_scalar_const);
      found_scalar_const = true;
    } else if (IsAddN(n, 2)) {
      EXPECT_FALSE(found_add);
      found_add = true;
    } else {
      ADD_FAILURE() << "Unexpected NodeDef: " << ProtoDebugString(n);
    }
  }
  EXPECT_TRUE(found_placeholder);
  EXPECT_TRUE(found_scalar_const);
  EXPECT_TRUE(found_add);

  // Add another oper to the graph.
  TF_Operation* neg = Neg(add, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Serialize to NodeDef.
  NodeDef node_def;
  ASSERT_TRUE(GetNodeDef(neg, &node_def));

  // Validate NodeDef is what we expect.
  EXPECT_TRUE(IsNeg(node_def, "add"));

  // Serialize to GraphDef.
  GraphDef graph_def2;
  ASSERT_TRUE(GetGraphDef(graph, &graph_def2));

  // Compare with first GraphDef + added NodeDef.
  NodeDef* added_node = graph_def.add_node();
  *added_node = node_def;
  EXPECT_EQ(ProtoDebugString(graph_def), ProtoDebugString(graph_def2));

  // Look up some nodes by name.
  TF_Operation* neg2 = TF_GraphOperationByName(graph, "neg");
  EXPECT_TRUE(neg == neg2);
  NodeDef node_def2;
  ASSERT_TRUE(GetNodeDef(neg2, &node_def2));
  EXPECT_EQ(ProtoDebugString(node_def), ProtoDebugString(node_def2));

  TF_Operation* feed2 = TF_GraphOperationByName(graph, "feed");
  EXPECT_TRUE(feed == feed2);
  ASSERT_TRUE(GetNodeDef(feed, &node_def));
  ASSERT_TRUE(GetNodeDef(feed2, &node_def2));
  EXPECT_EQ(ProtoDebugString(node_def), ProtoDebugString(node_def2));

  // Test iterating through the nodes of a graph.
  found_placeholder = false;
  found_scalar_const = false;
  found_add = false;
  bool found_neg = false;
  size_t pos = 0;
  TF_Operation* oper;
  while ((oper = TF_GraphNextOperation(graph, &pos)) != nullptr) {
    if (oper == feed) {
      EXPECT_FALSE(found_placeholder);
      found_placeholder = true;
    } else if (oper == three) {
      EXPECT_FALSE(found_scalar_const);
      found_scalar_const = true;
    } else if (oper == add) {
      EXPECT_FALSE(found_add);
      found_add = true;
    } else if (oper == neg) {
      EXPECT_FALSE(found_neg);
      found_neg = true;
    } else {
      ASSERT_TRUE(GetNodeDef(oper, &node_def));
      ADD_FAILURE() << "Unexpected Node: " << ProtoDebugString(node_def);
    }
  }
  EXPECT_TRUE(found_placeholder);
  EXPECT_TRUE(found_scalar_const);
  EXPECT_TRUE(found_add);
  EXPECT_TRUE(found_neg);

  // Clean up
  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

/*
TODO(skyewm): this test currently DCHECKs, change to bad status

TEST(CAPI, InputFromDifferentGraphError) {
  TF_Status* s = TF_NewStatus();
  TF_Graph* g1 = TF_NewGraph();
  TF_Graph* g2 = TF_NewGraph();

  TF_Operation* feed = Placeholder(g1, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Attempt to create node in g2 with input from g1
  Neg(feed, g2, s);
  EXPECT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s));
  EXPECT_STREQ("foo", TF_Message(s));

  TF_DeleteGraph(g1);
  TF_DeleteGraph(g2);
  TF_DeleteStatus(s);
}
*/

TEST(CAPI, ImportGraphDef) {
  TF_Status* s = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();

  // Create a simple graph.
  Placeholder(graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  ASSERT_TRUE(TF_GraphOperationByName(graph, "feed") != nullptr);
  TF_Operation* oper = ScalarConst(3, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  ASSERT_TRUE(TF_GraphOperationByName(graph, "scalar") != nullptr);
  Neg(oper, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  ASSERT_TRUE(TF_GraphOperationByName(graph, "neg") != nullptr);

  // Export to a GraphDef.
  TF_Buffer* graph_def = TF_NewBuffer();
  TF_GraphToGraphDef(graph, graph_def, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Import it, with a prefix, in a fresh graph.
  TF_DeleteGraph(graph);
  graph = TF_NewGraph();
  TF_ImportGraphDefOptions* opts = TF_NewImportGraphDefOptions();
  TF_ImportGraphDefOptionsSetPrefix(opts, "imported");
  TF_GraphImportGraphDef(graph, graph_def, opts, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* scalar = TF_GraphOperationByName(graph, "imported/scalar");
  TF_Operation* feed = TF_GraphOperationByName(graph, "imported/feed");
  TF_Operation* neg = TF_GraphOperationByName(graph, "imported/neg");
  ASSERT_TRUE(scalar != nullptr);
  ASSERT_TRUE(feed != nullptr);
  ASSERT_TRUE(neg != nullptr);

  // Test basic structure of the imported graph.
  EXPECT_EQ(0, TF_OperationNumInputs(scalar));
  EXPECT_EQ(0, TF_OperationNumInputs(feed));
  ASSERT_EQ(1, TF_OperationNumInputs(neg));
  TF_Output neg_input = TF_OperationInput({neg, 0});
  EXPECT_EQ(scalar, neg_input.oper);
  EXPECT_EQ(0, neg_input.index);

  // Test that we can't see control edges involving the source and sink nodes.
  TF_Operation* control_ops[100];
  EXPECT_EQ(0, TF_OperationNumControlInputs(scalar));
  EXPECT_EQ(0, TF_OperationGetControlInputs(scalar, control_ops, 100));
  EXPECT_EQ(0, TF_OperationNumControlOutputs(scalar));
  EXPECT_EQ(0, TF_OperationGetControlOutputs(scalar, control_ops, 100));

  EXPECT_EQ(0, TF_OperationNumControlInputs(feed));
  EXPECT_EQ(0, TF_OperationGetControlInputs(feed, control_ops, 100));
  EXPECT_EQ(0, TF_OperationNumControlOutputs(feed));
  EXPECT_EQ(0, TF_OperationGetControlOutputs(feed, control_ops, 100));

  EXPECT_EQ(0, TF_OperationNumControlInputs(neg));
  EXPECT_EQ(0, TF_OperationGetControlInputs(neg, control_ops, 100));
  EXPECT_EQ(0, TF_OperationNumControlOutputs(neg));
  EXPECT_EQ(0, TF_OperationGetControlOutputs(neg, control_ops, 100));

  // Import it again, with an input mapping, return outputs, and a return
  // operation, into the same graph.
  TF_DeleteImportGraphDefOptions(opts);
  opts = TF_NewImportGraphDefOptions();
  TF_ImportGraphDefOptionsSetPrefix(opts, "imported2");
  TF_ImportGraphDefOptionsAddInputMapping(opts, "scalar", 0, {scalar, 0});
  TF_ImportGraphDefOptionsAddReturnOutput(opts, "feed", 0);
  TF_ImportGraphDefOptionsAddReturnOutput(opts, "scalar", 0);
  EXPECT_EQ(2, TF_ImportGraphDefOptionsNumReturnOutputs(opts));
  TF_ImportGraphDefOptionsAddReturnOperation(opts, "scalar");
  EXPECT_EQ(1, TF_ImportGraphDefOptionsNumReturnOperations(opts));
  TF_ImportGraphDefResults* results =
      TF_GraphImportGraphDefWithResults(graph, graph_def, opts, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* scalar2 = TF_GraphOperationByName(graph, "imported2/scalar");
  TF_Operation* feed2 = TF_GraphOperationByName(graph, "imported2/feed");
  TF_Operation* neg2 = TF_GraphOperationByName(graph, "imported2/neg");
  ASSERT_TRUE(scalar2 != nullptr);
  ASSERT_TRUE(feed2 != nullptr);
  ASSERT_TRUE(neg2 != nullptr);

  // Check input mapping
  neg_input = TF_OperationInput({neg, 0});
  EXPECT_EQ(scalar, neg_input.oper);
  EXPECT_EQ(0, neg_input.index);

  // Check return outputs
  TF_Output* return_outputs;
  int num_return_outputs;
  TF_ImportGraphDefResultsReturnOutputs(results, &num_return_outputs,
                                        &return_outputs);
  ASSERT_EQ(2, num_return_outputs);
  EXPECT_EQ(feed2, return_outputs[0].oper);
  EXPECT_EQ(0, return_outputs[0].index);
  EXPECT_EQ(scalar, return_outputs[1].oper);  // remapped
  EXPECT_EQ(0, return_outputs[1].index);

  // Check return operation
  TF_Operation** return_opers;
  int num_return_opers;
  TF_ImportGraphDefResultsReturnOperations(results, &num_return_opers,
                                           &return_opers);
  ASSERT_EQ(1, num_return_opers);
  EXPECT_EQ(scalar2, return_opers[0]);  // not remapped

  TF_DeleteImportGraphDefResults(results);

  // Import again, with control dependencies, into the same graph.
  TF_DeleteImportGraphDefOptions(opts);
  opts = TF_NewImportGraphDefOptions();
  TF_ImportGraphDefOptionsSetPrefix(opts, "imported3");
  TF_ImportGraphDefOptionsAddControlDependency(opts, feed);
  TF_ImportGraphDefOptionsAddControlDependency(opts, feed2);
  TF_GraphImportGraphDef(graph, graph_def, opts, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* scalar3 = TF_GraphOperationByName(graph, "imported3/scalar");
  TF_Operation* feed3 = TF_GraphOperationByName(graph, "imported3/feed");
  TF_Operation* neg3 = TF_GraphOperationByName(graph, "imported3/neg");
  ASSERT_TRUE(scalar3 != nullptr);
  ASSERT_TRUE(feed3 != nullptr);
  ASSERT_TRUE(neg3 != nullptr);

  // Check that newly-imported scalar and feed have control deps (neg3 will
  // inherit them from input)
  TF_Operation* control_inputs[100];
  int num_control_inputs = TF_OperationGetControlInputs(
      scalar3, control_inputs, TF_OperationNumControlInputs(scalar3));
  ASSERT_EQ(2, num_control_inputs);
  EXPECT_EQ(feed, control_inputs[0]);
  EXPECT_EQ(feed2, control_inputs[1]);

  num_control_inputs = TF_OperationGetControlInputs(
      feed3, control_inputs, TF_OperationNumControlInputs(feed3));
  ASSERT_EQ(2, num_control_inputs);
  EXPECT_EQ(feed, control_inputs[0]);
  EXPECT_EQ(feed2, control_inputs[1]);

  // Export to a graph def so we can import a graph with control dependencies
  TF_DeleteBuffer(graph_def);
  graph_def = TF_NewBuffer();
  TF_GraphToGraphDef(graph, graph_def, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Import again, with remapped control dependency, into the same graph
  TF_DeleteImportGraphDefOptions(opts);
  opts = TF_NewImportGraphDefOptions();
  TF_ImportGraphDefOptionsSetPrefix(opts, "imported4");
  TF_ImportGraphDefOptionsRemapControlDependency(opts, "imported/feed", feed);
  TF_GraphImportGraphDef(graph, graph_def, opts, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* scalar4 =
      TF_GraphOperationByName(graph, "imported4/imported3/scalar");
  TF_Operation* feed4 =
      TF_GraphOperationByName(graph, "imported4/imported2/feed");

  // Check that imported `imported3/scalar` has remapped control dep from
  // original graph and imported control dep
  num_control_inputs = TF_OperationGetControlInputs(
      scalar4, control_inputs, TF_OperationNumControlInputs(scalar4));
  ASSERT_EQ(2, num_control_inputs);
  EXPECT_EQ(feed, control_inputs[0]);
  EXPECT_EQ(feed4, control_inputs[1]);

  TF_DeleteImportGraphDefOptions(opts);
  TF_DeleteBuffer(graph_def);

  // Can add nodes to the imported graph without trouble.
  Add(feed, scalar, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

TEST(CAPI, ImportGraphDef_WithReturnOutputs) {
  TF_Status* s = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();

  // Create a graph with two nodes: x and 3
  Placeholder(graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  ASSERT_TRUE(TF_GraphOperationByName(graph, "feed") != nullptr);
  TF_Operation* oper = ScalarConst(3, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  ASSERT_TRUE(TF_GraphOperationByName(graph, "scalar") != nullptr);
  Neg(oper, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  ASSERT_TRUE(TF_GraphOperationByName(graph, "neg") != nullptr);

  // Export to a GraphDef.
  TF_Buffer* graph_def = TF_NewBuffer();
  TF_GraphToGraphDef(graph, graph_def, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Import it in a fresh graph with return outputs.
  TF_DeleteGraph(graph);
  graph = TF_NewGraph();
  TF_ImportGraphDefOptions* opts = TF_NewImportGraphDefOptions();
  TF_ImportGraphDefOptionsAddReturnOutput(opts, "feed", 0);
  TF_ImportGraphDefOptionsAddReturnOutput(opts, "scalar", 0);
  EXPECT_EQ(2, TF_ImportGraphDefOptionsNumReturnOutputs(opts));
  TF_Output return_outputs[2];
  TF_GraphImportGraphDefWithReturnOutputs(graph, graph_def, opts,
                                          return_outputs, 2, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* scalar = TF_GraphOperationByName(graph, "scalar");
  TF_Operation* feed = TF_GraphOperationByName(graph, "feed");
  TF_Operation* neg = TF_GraphOperationByName(graph, "neg");
  ASSERT_TRUE(scalar != nullptr);
  ASSERT_TRUE(feed != nullptr);
  ASSERT_TRUE(neg != nullptr);

  // Check return outputs
  EXPECT_EQ(feed, return_outputs[0].oper);
  EXPECT_EQ(0, return_outputs[0].index);
  EXPECT_EQ(scalar, return_outputs[1].oper);
  EXPECT_EQ(0, return_outputs[1].index);

  TF_DeleteImportGraphDefOptions(opts);
  TF_DeleteBuffer(graph_def);
  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

TEST(CAPI, ImportGraphDef_MissingUnusedInputMappings) {
  TF_Status* s = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();

  // Create a graph with two nodes: x and 3
  Placeholder(graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  ASSERT_TRUE(TF_GraphOperationByName(graph, "feed") != nullptr);
  TF_Operation* oper = ScalarConst(3, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  ASSERT_TRUE(TF_GraphOperationByName(graph, "scalar") != nullptr);
  Neg(oper, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  ASSERT_TRUE(TF_GraphOperationByName(graph, "neg") != nullptr);

  // Export to a GraphDef.
  TF_Buffer* graph_def = TF_NewBuffer();
  TF_GraphToGraphDef(graph, graph_def, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Import it in a fresh graph.
  TF_DeleteGraph(graph);
  graph = TF_NewGraph();
  TF_ImportGraphDefOptions* opts = TF_NewImportGraphDefOptions();
  TF_GraphImportGraphDef(graph, graph_def, opts, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* scalar = TF_GraphOperationByName(graph, "scalar");

  // Import it in a fresh graph with an unused input mapping.
  TF_DeleteImportGraphDefOptions(opts);
  opts = TF_NewImportGraphDefOptions();
  TF_ImportGraphDefOptionsSetPrefix(opts, "imported");
  TF_ImportGraphDefOptionsAddInputMapping(opts, "scalar", 0, {scalar, 0});
  TF_ImportGraphDefOptionsAddInputMapping(opts, "fake", 0, {scalar, 0});
  TF_ImportGraphDefResults* results =
      TF_GraphImportGraphDefWithResults(graph, graph_def, opts, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Check unused input mappings
  int num_unused_input_mappings;
  const char** src_names;
  int* src_indexes;
  TF_ImportGraphDefResultsMissingUnusedInputMappings(
      results, &num_unused_input_mappings, &src_names, &src_indexes);
  ASSERT_EQ(1, num_unused_input_mappings);
  EXPECT_EQ(string("fake"), string(src_names[0]));
  EXPECT_EQ(0, src_indexes[0]);

  TF_DeleteImportGraphDefResults(results);
  TF_DeleteImportGraphDefOptions(opts);
  TF_DeleteBuffer(graph_def);
  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

TEST(CAPI, Session) {
  TF_Status* s = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();

  // Make a placeholder operation.
  TF_Operation* feed = Placeholder(graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Make a constant operation with the scalar "2".
  TF_Operation* two = ScalarConst(2, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Add operation.
  TF_Operation* add = Add(feed, two, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Create a session for this graph.
  CSession csession(graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Run the graph.
  csession.SetInputs({{feed, Int32Tensor(3)}});
  csession.SetOutputs({add});
  csession.Run(s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_Tensor* out = csession.output_tensor(0);
  ASSERT_TRUE(out != nullptr);
  EXPECT_EQ(TF_INT32, TF_TensorType(out));
  EXPECT_EQ(0, TF_NumDims(out));  // scalar
  ASSERT_EQ(sizeof(int32), TF_TensorByteSize(out));
  int32* output_contents = static_cast<int32*>(TF_TensorData(out));
  EXPECT_EQ(3 + 2, *output_contents);

  // Add another operation to the graph.
  TF_Operation* neg = Neg(add, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Run up to the new operation.
  csession.SetInputs({{feed, Int32Tensor(7)}});
  csession.SetOutputs({neg});
  csession.Run(s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  out = csession.output_tensor(0);
  ASSERT_TRUE(out != nullptr);
  EXPECT_EQ(TF_INT32, TF_TensorType(out));
  EXPECT_EQ(0, TF_NumDims(out));  // scalar
  ASSERT_EQ(sizeof(int32), TF_TensorByteSize(out));
  output_contents = static_cast<int32*>(TF_TensorData(out));
  EXPECT_EQ(-(7 + 2), *output_contents);

  // Clean up
  csession.CloseAndDelete(s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

// If `device` is non-empty, run Min op on that device.
// Otherwise run it on the default device (CPU).
void RunMinTest(const string& device, bool use_XLA) {
  TF_Status* s = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();

  // Make a placeholder operation.
  TF_Operation* feed = Placeholder(graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Make a constant operation with the scalar "0", for axis.
  TF_Operation* one = ScalarConst(0, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Create a session for this graph.
  CSession csession(graph, s, use_XLA);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  if (!device.empty()) {
    LOG(INFO) << "Setting op Min on device " << device;
  }
  TF_Operation* min = MinWithDevice(feed, one, graph, device, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Run the graph.
  csession.SetInputs({{feed, Int32Tensor({3, 2, 5})}});
  csession.SetOutputs({min});
  csession.Run(s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_Tensor* out = csession.output_tensor(0);
  ASSERT_TRUE(out != nullptr);
  EXPECT_EQ(TF_INT32, TF_TensorType(out));
  EXPECT_EQ(0, TF_NumDims(out));  // scalar
  ASSERT_EQ(sizeof(int32), TF_TensorByteSize(out));
  int32* output_contents = static_cast<int32*>(TF_TensorData(out));
  EXPECT_EQ(2, *output_contents);

  // Clean up
  csession.CloseAndDelete(s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

TEST(CAPI, Session_Min_CPU) { RunMinTest(/*device=*/"", /*use_XLA=*/false); }

TEST(CAPI, Session_Min_XLA_CPU) { RunMinTest(/*device=*/"", /*use_XLA=*/true); }

TEST(CAPI, Session_Min_GPU) {
  const string gpu_device = GPUDeviceName();
  // Skip this test if no GPU is available.
  if (gpu_device.empty()) return;

  RunMinTest(gpu_device, /*use_XLA=*/false);
}

TEST(CAPI, Session_Min_XLA_GPU) {
  const string gpu_device = GPUDeviceName();
  // Skip this test if no GPU is available.
  if (gpu_device.empty()) return;

  RunMinTest(gpu_device, /*use_XLA=*/true);
}

TEST(CAPI, SessionPRun) {
  TF_Status* s = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();

  // Construct the graph: A + 2 + B
  TF_Operation* a = Placeholder(graph, s, "A");
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* b = Placeholder(graph, s, "B");
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* two = ScalarConst(2, graph, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* plus2 = Add(a, two, graph, s, "plus2");
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Operation* plusB = Add(plus2, b, graph, s, "plusB");
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Setup a session and a partial run handle.  The partial run will allow
  // computation of A + 2 + B in two phases (calls to TF_SessionPRun):
  // 1. Feed A and get (A+2)
  // 2. Feed B and get (A+2)+B
  TF_SessionOptions* opts = TF_NewSessionOptions();
  TF_Session* sess = TF_NewSession(graph, opts, s);
  TF_DeleteSessionOptions(opts);

  TF_Output feeds[] = {TF_Output{a, 0}, TF_Output{b, 0}};
  TF_Output fetches[] = {TF_Output{plus2, 0}, TF_Output{plusB, 0}};

  const char* handle = nullptr;
  TF_SessionPRunSetup(sess, feeds, TF_ARRAYSIZE(feeds), fetches,
                      TF_ARRAYSIZE(fetches), nullptr, 0, &handle, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  // Feed A and fetch A + 2.
  TF_Output feeds1[] = {TF_Output{a, 0}};
  TF_Output fetches1[] = {TF_Output{plus2, 0}};
  TF_Tensor* feedValues1[] = {Int32Tensor(1)};
  TF_Tensor* fetchValues1[1];
  TF_SessionPRun(sess, handle, feeds1, feedValues1, 1, fetches1, fetchValues1,
                 1, nullptr, 0, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(3, *(static_cast<int32*>(TF_TensorData(fetchValues1[0]))));
  TF_DeleteTensor(feedValues1[0]);
  TF_DeleteTensor(fetchValues1[0]);

  // Feed B and fetch (A + 2) + B.
  TF_Output feeds2[] = {TF_Output{b, 0}};
  TF_Output fetches2[] = {TF_Output{plusB, 0}};
  TF_Tensor* feedValues2[] = {Int32Tensor(4)};
  TF_Tensor* fetchValues2[1];
  TF_SessionPRun(sess, handle, feeds2, feedValues2, 1, fetches2, fetchValues2,
                 1, nullptr, 0, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  EXPECT_EQ(7, *(static_cast<int32*>(TF_TensorData(fetchValues2[0]))));
  TF_DeleteTensor(feedValues2[0]);
  TF_DeleteTensor(fetchValues2[0]);

  // Clean up.
  TF_DeletePRunHandle(handle);
  TF_DeleteSession(sess, s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

TEST(CAPI, ShapeInferenceError) {
  // TF_FinishOperation should fail if the shape of the added operation cannot
  // be inferred.
  TF_Status* status = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();

  // Create this failure by trying to add two nodes with incompatible shapes
  // (A tensor with shape [2] and a tensor with shape [3] cannot be added).
  const char data[] = {1, 2, 3};
  const int64_t vec2_dims[] = {2};
  unique_tensor_ptr vec2_tensor(
      Int8Tensor(vec2_dims, TF_ARRAYSIZE(vec2_dims), data), TF_DeleteTensor);
  TF_Operation* vec2 = Const(vec2_tensor.get(), graph, status, "vec2");
  ASSERT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);

  const int64_t vec3_dims[] = {3};
  unique_tensor_ptr vec3_tensor(
      Int8Tensor(vec3_dims, TF_ARRAYSIZE(vec3_dims), data), TF_DeleteTensor);
  TF_Operation* vec3 = Const(vec3_tensor.get(), graph, status, "vec3");
  ASSERT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);

  TF_Operation* add = AddNoCheck(vec2, vec3, graph, status);
  ASSERT_NE(TF_OK, TF_GetCode(status));
  ASSERT_TRUE(add == nullptr);

  TF_DeleteGraph(graph);
  TF_DeleteStatus(status);
}

TEST(CAPI, GetOpDef) {
  TF_Status* status = TF_NewStatus();
  TF_Graph* graph = TF_NewGraph();
  TF_Buffer* buffer = TF_NewBuffer();

  TF_GraphGetOpDef(graph, "Add", buffer, status);
  ASSERT_EQ(TF_OK, TF_GetCode(status));
  const OpDef* expected_op_def;
  TF_ASSERT_OK(OpRegistry::Global()->LookUpOpDef("Add", &expected_op_def));
  string expected_serialized;
  expected_op_def->SerializeToString(&expected_serialized);
  string actual_string(reinterpret_cast<const char*>(buffer->data),
                       buffer->length);
  EXPECT_EQ(expected_serialized, actual_string);

  TF_GraphGetOpDef(graph, "MyFakeOp", buffer, status);
  EXPECT_EQ(TF_NOT_FOUND, TF_GetCode(status));
  ExpectHasSubstr(TF_Message(status),
                  "Op type not registered 'MyFakeOp' in binary");

  TF_DeleteBuffer(buffer);
  TF_DeleteGraph(graph);
  TF_DeleteStatus(status);
}

void StringVectorToArrays(const std::vector<string>& v,
                          std::unique_ptr<const void*[]>* ptrs,
                          std::unique_ptr<size_t[]>* lens) {
  ptrs->reset(new const void*[v.size()]);
  lens->reset(new size_t[v.size()]);
  for (size_t i = 0; i < v.size(); ++i) {
    (*ptrs)[i] = v[i].data();
    (*lens)[i] = v[i].size();
  }
}

class CApiColocationTest : public ::testing::Test {
 protected:
  CApiColocationTest() : s_(TF_NewStatus()), graph_(TF_NewGraph()) {}

  void SetUp() override {
    feed1_ = Placeholder(graph_, s_, "feed1");
    ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

    feed2_ = Placeholder(graph_, s_, "feed2");
    ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

    constant_ = ScalarConst(10, graph_, s_);
    ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

    desc_ = TF_NewOperation(graph_, "AddN", "add");
    TF_Output inputs[] = {{feed1_, 0}, {constant_, 0}};
    TF_AddInputList(desc_, inputs, TF_ARRAYSIZE(inputs));
  }

  ~CApiColocationTest() override {
    TF_DeleteGraph(graph_);
    TF_DeleteStatus(s_);
  }

  void SetViaStringList(TF_OperationDescription* desc,
                        const std::vector<string>& list) {
    std::unique_ptr<const void*[]> list_ptrs;
    std::unique_ptr<size_t[]> list_lens;
    StringVectorToArrays(list, &list_ptrs, &list_lens);
    TF_SetAttrStringList(desc, tensorflow::kColocationAttrName, list_ptrs.get(),
                         list_lens.get(), list.size());
  }

  void SetViaProto(TF_OperationDescription* desc,
                   const std::vector<string>& list) {
    tensorflow::AttrValue attr;
    for (const string& v : list) {
      attr.mutable_list()->add_s(v);
    }
    string bytes;
    attr.SerializeToString(&bytes);
    TF_SetAttrValueProto(desc, tensorflow::kColocationAttrName, bytes.data(),
                         bytes.size(), s_);
    ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  }

  void VerifyCollocation(TF_Operation* op,
                         const std::vector<string>& expected) {
    TF_AttrMetadata m =
        TF_OperationGetAttrMetadata(op, tensorflow::kColocationAttrName, s_);
    if (expected.empty()) {
      ASSERT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s_)) << TF_Message(s_);
      EXPECT_EQ("Operation 'add' has no attr named '_class'.",
                string(TF_Message(s_)));
      return;
    }
    EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
    EXPECT_EQ(1, m.is_list);
    EXPECT_EQ(expected.size(), m.list_size);
    EXPECT_EQ(TF_ATTR_STRING, m.type);
    std::vector<void*> values(expected.size());
    std::vector<size_t> lens(expected.size());
    std::unique_ptr<char[]> storage(new char[m.total_size]);
    TF_OperationGetAttrStringList(op, tensorflow::kColocationAttrName,
                                  values.data(), lens.data(), expected.size(),
                                  storage.get(), m.total_size, s_);
    EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
    for (int i = 0; i < expected.size(); ++i) {
      EXPECT_EQ(expected[i],
                string(static_cast<const char*>(values[i]), lens[i]));
    }
  }

  void FinishAndVerify(TF_OperationDescription* desc,
                       const std::vector<string>& expected) {
    TF_Operation* op = TF_FinishOperation(desc_, s_);
    ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
    VerifyCollocation(op, expected);
  }

  TF_Status* s_;
  TF_Graph* graph_;
  TF_Operation* feed1_;
  TF_Operation* feed2_;
  TF_Operation* constant_;
  TF_OperationDescription* desc_;
};

TEST_F(CApiColocationTest, ColocateWith) {
  TF_ColocateWith(desc_, feed1_);
  FinishAndVerify(desc_, {"loc:@feed1"});
}

TEST_F(CApiColocationTest, StringList) {
  SetViaStringList(desc_, {"loc:@feed1"});
  FinishAndVerify(desc_, {"loc:@feed1"});
}

TEST_F(CApiColocationTest, Proto) {
  SetViaProto(desc_, {"loc:@feed1"});
  FinishAndVerify(desc_, {"loc:@feed1"});
}

TEST_F(CApiColocationTest, ColocateWith_StringList) {
  TF_ColocateWith(desc_, feed1_);
  SetViaStringList(desc_, {"loc:@feed2"});
  FinishAndVerify(desc_, {"loc:@feed2"});
}

TEST_F(CApiColocationTest, ColocateWith_Proto) {
  TF_ColocateWith(desc_, feed1_);
  SetViaProto(desc_, {"loc:@feed2"});
  FinishAndVerify(desc_, {"loc:@feed2"});
}

TEST_F(CApiColocationTest, StringList_ColocateWith) {
  SetViaStringList(desc_, {"loc:@feed2"});
  TF_ColocateWith(desc_, feed1_);
  FinishAndVerify(desc_, {"loc:@feed1", "loc:@feed2"});
}

TEST_F(CApiColocationTest, Proto_ColocateWith) {
  SetViaProto(desc_, {"loc:@feed2"});
  TF_ColocateWith(desc_, feed1_);
  FinishAndVerify(desc_, {"loc:@feed1", "loc:@feed2"});
}

TEST_F(CApiColocationTest, ColocateWith_ColocateWith) {
  TF_ColocateWith(desc_, feed1_);
  TF_ColocateWith(desc_, feed2_);
  FinishAndVerify(desc_, {"loc:@feed1", "loc:@feed2"});
}

TEST_F(CApiColocationTest, Proto_StringList) {
  SetViaProto(desc_, {"loc:@feed1"});
  SetViaStringList(desc_, {"loc:@feed2"});
  FinishAndVerify(desc_, {"loc:@feed2"});
}

TEST_F(CApiColocationTest, StringList_Proto) {
  SetViaStringList(desc_, {"loc:@feed1"});
  SetViaProto(desc_, {"loc:@feed2"});
  FinishAndVerify(desc_, {"loc:@feed2"});
}

TEST_F(CApiColocationTest, ClearViaStringList) {
  TF_ColocateWith(desc_, feed1_);
  SetViaStringList(desc_, {});
  FinishAndVerify(desc_, {});
}

TEST_F(CApiColocationTest, ClearViaProto) {
  TF_ColocateWith(desc_, feed1_);
  SetViaProto(desc_, {});
  FinishAndVerify(desc_, {});
}

TEST(CAPI, SavedModel) {
  // Load the saved model.
  const char kSavedModel[] = "cc/saved_model/testdata/half_plus_two/00000123";
  const string saved_model_dir = tensorflow::io::JoinPath(
      tensorflow::testing::TensorFlowSrcRoot(), kSavedModel);
  TF_SessionOptions* opt = TF_NewSessionOptions();
  TF_Buffer* run_options = TF_NewBufferFromString("", 0);
  TF_Buffer* metagraph = TF_NewBuffer();
  TF_Status* s = TF_NewStatus();
  const char* tags[] = {tensorflow::kSavedModelTagServe};
  TF_Graph* graph = TF_NewGraph();
  TF_Session* session = TF_LoadSessionFromSavedModel(
      opt, run_options, saved_model_dir.c_str(), tags, 1, graph, metagraph, s);
  TF_DeleteBuffer(run_options);
  TF_DeleteSessionOptions(opt);
  tensorflow::MetaGraphDef metagraph_def;
  metagraph_def.ParseFromArray(metagraph->data, metagraph->length);
  TF_DeleteBuffer(metagraph);

  EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  CSession csession(session);

  // Retrieve the regression signature from meta graph def.
  const auto signature_def_map = metagraph_def.signature_def();
  const auto signature_def = signature_def_map.at("regress_x_to_y");

  const string input_name =
      signature_def.inputs().at(tensorflow::kRegressInputs).name();
  const string output_name =
      signature_def.outputs().at(tensorflow::kRegressOutputs).name();

  // Write {0, 1, 2, 3} as tensorflow::Example inputs.
  Tensor input(tensorflow::DT_STRING, TensorShape({4}));
  for (tensorflow::int64 i = 0; i < input.NumElements(); ++i) {
    tensorflow::Example example;
    auto* feature_map = example.mutable_features()->mutable_feature();
    (*feature_map)["x"].mutable_float_list()->add_value(i);
    input.flat<tstring>()(i) = example.SerializeAsString();
  }

  const tensorflow::string input_op_name(
      tensorflow::ParseTensorName(input_name).first);
  TF_Operation* input_op =
      TF_GraphOperationByName(graph, input_op_name.c_str());
  ASSERT_TRUE(input_op != nullptr);
  csession.SetInputs({{input_op, TF_TensorFromTensor(input, s)}});
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  const tensorflow::string output_op_name(
      tensorflow::ParseTensorName(output_name).first);
  TF_Operation* output_op =
      TF_GraphOperationByName(graph, output_op_name.c_str());
  ASSERT_TRUE(output_op != nullptr);
  csession.SetOutputs({output_op});
  csession.Run(s);
  ASSERT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);

  TF_Tensor* out = csession.output_tensor(0);
  ASSERT_TRUE(out != nullptr);
  EXPECT_EQ(TF_FLOAT, TF_TensorType(out));
  EXPECT_EQ(2, TF_NumDims(out));
  EXPECT_EQ(4, TF_Dim(out, 0));
  EXPECT_EQ(1, TF_Dim(out, 1));
  float* values = static_cast<float*>(TF_TensorData(out));
  // These values are defined to be (input / 2) + 2.
  EXPECT_EQ(2, values[0]);
  EXPECT_EQ(2.5, values[1]);
  EXPECT_EQ(3, values[2]);
  EXPECT_EQ(3.5, values[3]);

  csession.CloseAndDelete(s);
  EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

TEST(CAPI, SavedModelNullArgsAreValid) {
  const char kSavedModel[] = "cc/saved_model/testdata/half_plus_two/00000123";
  const string saved_model_dir = tensorflow::io::JoinPath(
      tensorflow::testing::TensorFlowSrcRoot(), kSavedModel);
  TF_SessionOptions* opt = TF_NewSessionOptions();
  TF_Status* s = TF_NewStatus();
  const char* tags[] = {tensorflow::kSavedModelTagServe};
  TF_Graph* graph = TF_NewGraph();
  // NULL run_options and meta_graph_def should work.
  TF_Session* session = TF_LoadSessionFromSavedModel(
      opt, nullptr, saved_model_dir.c_str(), tags, 1, graph, nullptr, s);
  EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_DeleteSessionOptions(opt);
  TF_CloseSession(session, s);
  EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_DeleteSession(session, s);
  EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
  TF_DeleteGraph(graph);
  TF_DeleteStatus(s);
}

TEST(CAPI, DeletingNullPointerIsSafe) {
  TF_Status* status = TF_NewStatus();

  TF_DeleteStatus(nullptr);
  TF_DeleteBuffer(nullptr);
  TF_DeleteTensor(nullptr);
  TF_DeleteSessionOptions(nullptr);
  TF_DeleteGraph(nullptr);
  TF_DeleteImportGraphDefOptions(nullptr);
  TF_DeleteImportGraphDefResults(nullptr);
  TF_DeleteFunction(nullptr);
  TF_DeleteSession(nullptr, status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  TF_DeletePRunHandle(nullptr);
  TF_DeleteDeprecatedSession(nullptr, status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  TF_DeleteDeviceList(nullptr);
  TF_DeleteLibraryHandle(nullptr);
  TF_DeleteApiDefMap(nullptr);

  TF_DeleteStatus(status);
}

TEST(CAPI, TestBitcastFrom_Reshape) {
  int64_t dims[] = {2, 3};
  TF_Tensor* a =
      TF_AllocateTensor(TF_UINT64, dims, 2, 6 * TF_DataTypeSize(TF_UINT64));
  TF_Tensor* b =
      TF_AllocateTensor(TF_UINT64, nullptr, 0, TF_DataTypeSize(TF_UINT64));
  EXPECT_NE(a, nullptr);
  EXPECT_NE(b, nullptr);

  EXPECT_EQ(6, TF_TensorElementCount(a));
  EXPECT_EQ(1, TF_TensorElementCount(b));
  EXPECT_EQ(6 * TF_DataTypeSize(TF_UINT64), TF_TensorByteSize(a));
  EXPECT_EQ(TF_DataTypeSize(TF_UINT64), TF_TensorByteSize(b));

  int64_t new_dims[] = {3, 2};
  TF_Status* status = TF_NewStatus();
  TF_TensorBitcastFrom(a, TF_UINT64, b, new_dims, 2, status);
  ASSERT_EQ(TF_OK, TF_GetCode(status));
  TF_DeleteStatus(status);

  EXPECT_EQ(6, TF_TensorElementCount(a));
  EXPECT_EQ(6, TF_TensorElementCount(b));
  EXPECT_EQ(6 * TF_DataTypeSize(TF_UINT64), TF_TensorByteSize(a));
  EXPECT_EQ(6 * TF_DataTypeSize(TF_UINT64), TF_TensorByteSize(b));

  // Check that a write to one tensor shows up in the other.
  *(static_cast<int64_t*>(TF_TensorData(a))) = 4;
  EXPECT_EQ(4, *(static_cast<int64_t*>(TF_TensorData(b))));
  *(static_cast<int64_t*>(TF_TensorData(b))) = 6;
  EXPECT_EQ(6, *(static_cast<int64_t*>(TF_TensorData(a))));

  TF_DeleteTensor(a);
  TF_DeleteTensor(b);
}

REGISTER_OP("TestOpWithNoGradient")
    .Input("x: T")
    .Output("y: T")
    .Attr("T: {float, double}")
    .Doc(R"doc(
Test op with no grad registered.

x: input
y: output
)doc")
    .SetShapeFn(tensorflow::shape_inference::UnknownShape);

class CApiGradientsTest : public ::testing::Test {
 protected:
  CApiGradientsTest()
      : s_(TF_NewStatus()),
        graph_(TF_NewGraph()),
        expected_graph_(TF_NewGraph()) {}

  ~CApiGradientsTest() override {
    TF_DeleteGraph(graph_);
    TF_DeleteGraph(expected_graph_);
    TF_DeleteStatus(s_);
  }

  void TestGradientsSuccess(bool grad_inputs_provided) {
    TF_Output inputs[2];
    TF_Output outputs[1];
    TF_Output grad_outputs[2];
    TF_Output expected_grad_outputs[2];

    BuildSuccessGraph(inputs, outputs);
    BuildExpectedGraph(grad_inputs_provided, expected_grad_outputs);

    AddGradients(grad_inputs_provided, nullptr, inputs, 2, outputs, 1,
                 grad_outputs);
    EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

    // Compare that the graphs match.
    GraphDef expected_gdef;
    GraphDef gdef;
    EXPECT_TRUE(GetGraphDef(expected_graph_, &expected_gdef));
    EXPECT_TRUE(GetGraphDef(graph_, &gdef));
    TF_EXPECT_GRAPH_EQ(expected_gdef, gdef);

    // Compare that the output of the gradients of both graphs match.
    RunGraphsAndCompareOutputs(grad_outputs, expected_grad_outputs);
  }

  void TestGradientsError(bool grad_inputs_provided) {
    TF_Output inputs[1];
    TF_Output outputs[1];
    TF_Output grad_outputs[1];

    BuildErrorGraph(inputs, outputs);

    AddGradients(grad_inputs_provided, nullptr, inputs, 1, outputs, 1,
                 grad_outputs);

    string expected_msg =
        "No gradient defined for op: TestOpWithNoGradient. Please see "
        "https://www.tensorflow.org/code/"
        "tensorflow/cc/gradients/README.md"
        " for instructions on how to add C++ gradients.";
    EXPECT_EQ(expected_msg, TF_Message(s_));
  }

  // Run the graph and ensure that the gradient values are as expected.
  void RunGraphsAndCompareOutputs(TF_Output* grad_outputs,
                                  TF_Output* expected_grad_outputs) {
    std::unique_ptr<CSession> csession(new CSession(graph_, s_));
    std::unique_ptr<CSession> expected_csession(
        new CSession(expected_graph_, s_));

    std::vector<TF_Output> grad_outputs_vec;
    grad_outputs_vec.assign(grad_outputs, grad_outputs + 2);
    csession->SetOutputs(grad_outputs_vec);
    csession->Run(s_);
    ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
    TF_Tensor* out0 = csession->output_tensor(0);
    TF_Tensor* out1 = csession->output_tensor(1);

    std::vector<TF_Output> expected_grad_outputs_vec;
    expected_grad_outputs_vec.assign(expected_grad_outputs,
                                     expected_grad_outputs + 2);
    expected_csession->SetOutputs(expected_grad_outputs_vec);
    expected_csession->Run(s_);
    ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
    TF_Tensor* expected_out0 = expected_csession->output_tensor(0);
    TF_Tensor* expected_out1 = expected_csession->output_tensor(1);

    CompareTensors(out0, expected_out0);
    CompareTensors(out1, expected_out1);
  }

  void CompareTensors(TF_Tensor* a, TF_Tensor* b) {
    float* a_data = static_cast<float*>(TF_TensorData(a));
    float* b_data = static_cast<float*>(TF_TensorData(b));
    EXPECT_EQ(*a_data, *b_data);
  }

  void AddGradients(bool grad_inputs_provided, const char* prefix,
                    TF_Output* inputs, int ninputs, TF_Output* outputs,
                    int noutputs, TF_Output* grad_outputs) {
    if (grad_inputs_provided) {
      TF_Output grad_inputs[1];
      const float grad_inputs_val[] = {1.0, 1.0, 1.0, 1.0};
      TF_Operation* grad_inputs_op =
          FloatConst2x2(graph_, s_, grad_inputs_val, "GradInputs");
      grad_inputs[0] = TF_Output{grad_inputs_op, 0};
      TF_AddGradientsWithPrefix(graph_, prefix, outputs, noutputs, inputs,
                                ninputs, grad_inputs, s_, grad_outputs);
    } else {
      TF_AddGradientsWithPrefix(graph_, prefix, outputs, noutputs, inputs,
                                ninputs, nullptr, s_, grad_outputs);
    }
  }

  void BuildErrorGraph(TF_Output* inputs, TF_Output* outputs) {
    const float const0_val[] = {1.0, 2.0, 3.0, 4.0};
    TF_Operation* const0 = FloatConst2x2(graph_, s_, const0_val, "Const_0");
    TF_Operation* nograd = NoGradientOp(graph_, s_, const0, "NoGrad");
    inputs[0] = TF_Output{const0, 0};
    outputs[0] = TF_Output{nograd, 0};
    EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  }

  void BuildSuccessGraph(TF_Output* inputs, TF_Output* outputs) {
    // Construct the following graph:
    //            |
    //           z|
    //            |
    //          MatMul
    //         /       \
    //        ^         ^
    //        |         |
    //       x|        y|
    //        |         |
    //        |         |
    //      Const_0    Const_1
    //
    const float const0_val[] = {1.0, 2.0, 3.0, 4.0};
    const float const1_val[] = {1.0, 0.0, 0.0, 1.0};
    TF_Operation* const0 = FloatConst2x2(graph_, s_, const0_val, "Const_0");
    TF_Operation* const1 = FloatConst2x2(graph_, s_, const1_val, "Const_1");
    TF_Operation* matmul = MatMul(graph_, s_, const0, const1, "MatMul");
    inputs[0] = TF_Output{const0, 0};
    inputs[1] = TF_Output{const1, 0};
    outputs[0] = TF_Output{matmul, 0};
    EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  }

  void BuildExpectedGraph(bool grad_inputs_provided,
                          TF_Output* expected_grad_outputs) {
    // The expected graph looks like this if grad_inputs_provided.
    // If grad_inputs_provided is false, Const_0 will be a OnesLike op.
    //      ^             ^
    //    dy|           dx|        // MatMul Gradient Graph
    //      |             |
    //   MatMul_2      MatMul_1
    //   ^   ^          ^    ^
    //   |   |----------|    |
    //   |        ^          |
    //   |      dz|          |
    //   |        |          |
    //   |     Const_3       |
    //   |                   |
    //   |        ^          |
    //   |       z|          |     // MatMul Forward Graph
    //   |        |          |
    //   |      MatMul       |
    //   |     /       \     |
    //   |    ^         ^    |
    //   |    |         |    |
    //   |---x|        y|----|
    //        |         |
    //        |         |
    //      Const_0   Const_1
    //
    const float const0_val[] = {1.0, 2.0, 3.0, 4.0};
    const float const1_val[] = {1.0, 0.0, 0.0, 1.0};
    TF_Operation* const0 =
        FloatConst2x2(expected_graph_, s_, const0_val, "Const_0");
    TF_Operation* const1 =
        FloatConst2x2(expected_graph_, s_, const1_val, "Const_1");
    TF_Operation* matmul =
        MatMul(expected_graph_, s_, const0, const1, "MatMul");

    TF_Operation* const3;
    if (grad_inputs_provided) {
      const float const3_val[] = {1.0, 1.0, 1.0, 1.0};
      const3 = FloatConst2x2(expected_graph_, s_, const3_val, "GradInputs");
    } else {
      const3 = OnesLike(expected_graph_, s_, matmul, "gradients/OnesLike");
    }

    TF_Operation* matmul1 = MatMul(expected_graph_, s_, const3, const1,
                                   "gradients/MatMul", false, true);
    TF_Operation* matmul2 = MatMul(expected_graph_, s_, const0, const3,
                                   "gradients/MatMul_1", true, false);
    expected_grad_outputs[0] = {matmul1, 0};
    expected_grad_outputs[1] = {matmul2, 0};
  }

  TF_Tensor* FloatTensor2x2(const float* values) {
    const int64_t dims[2] = {2, 2};
    TF_Tensor* t = TF_AllocateTensor(TF_FLOAT, dims, 2, sizeof(float) * 4);
    memcpy(TF_TensorData(t), values, sizeof(float) * 4);
    return t;
  }

  TF_Operation* FloatConst2x2(TF_Graph* graph, TF_Status* s,
                              const float* values, const char* name) {
    unique_tensor_ptr tensor(FloatTensor2x2(values), TF_DeleteTensor);
    TF_OperationDescription* desc = TF_NewOperation(graph, "Const", name);
    TF_SetAttrTensor(desc, "value", tensor.get(), s);
    if (TF_GetCode(s) != TF_OK) return nullptr;
    TF_SetAttrType(desc, "dtype", TF_FLOAT);
    TF_Operation* op = TF_FinishOperation(desc, s);
    EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
    return op;
  }

  TF_Operation* MatMul(TF_Graph* graph, TF_Status* s, TF_Operation* l,
                       TF_Operation* r, const char* name,
                       bool transpose_a = false, bool transpose_b = false) {
    TF_OperationDescription* desc = TF_NewOperation(graph, "MatMul", name);
    if (transpose_a) {
      TF_SetAttrBool(desc, "transpose_a", 1);
    }
    if (transpose_b) {
      TF_SetAttrBool(desc, "transpose_b", 1);
    }
    TF_AddInput(desc, {l, 0});
    TF_AddInput(desc, {r, 0});
    TF_Operation* op = TF_FinishOperation(desc, s);
    EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
    return op;
  }

  TF_Operation* OnesLike(TF_Graph* graph, TF_Status* s, TF_Operation* in,
                         const char* name) {
    TF_OperationDescription* desc = TF_NewOperation(graph, "OnesLike", name);
    TF_AddInput(desc, {in, 0});
    TF_Operation* op = TF_FinishOperation(desc, s);
    EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
    return op;
  }

  TF_Operation* NoGradientOp(TF_Graph* graph, TF_Status* s, TF_Operation* in,
                             const char* name) {
    TF_OperationDescription* desc =
        TF_NewOperation(graph, "TestOpWithNoGradient", name);
    TF_AddInput(desc, {in, 0});
    TF_Operation* op = TF_FinishOperation(desc, s);
    EXPECT_EQ(TF_OK, TF_GetCode(s)) << TF_Message(s);
    return op;
  }

  void BuildGraphAndAddGradientsWithPrefixes(const char* prefix1,
                                             const char* prefix2 = nullptr) {
    TF_Output inputs[2];
    TF_Output outputs[1];
    TF_Output grad_outputs[2];

    BuildSuccessGraph(inputs, outputs);

    AddGradients(false, prefix1, inputs, 2, outputs, 1, grad_outputs);
    if (prefix2 != nullptr) {
      AddGradients(false, prefix2, inputs, 2, outputs, 1, grad_outputs);
    }
  }

  TF_Status* s_;
  TF_Graph* graph_;
  TF_Graph* expected_graph_;
};

TEST_F(CApiGradientsTest, Gradients_GradInputs) { TestGradientsSuccess(true); }

TEST_F(CApiGradientsTest, Gradients_NoGradInputs) {
  TestGradientsSuccess(false);
}

TEST_F(CApiGradientsTest, OpWithNoGradientRegistered_GradInputs) {
  TestGradientsError(true);
}

TEST_F(CApiGradientsTest, OpWithNoGradientRegistered_NoGradInputs) {
  TestGradientsError(false);
}

TEST_F(CApiGradientsTest, GradientsPrefix_PrefixIsOk) {
  BuildGraphAndAddGradientsWithPrefixes("gradients");
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
}

TEST_F(CApiGradientsTest, GradientsPrefix_TwoGradientsWithDistinctPrefixes) {
  BuildGraphAndAddGradientsWithPrefixes("gradients", "gradients_1");
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
}

TEST_F(CApiGradientsTest, GradientsPrefix_TwoGradientsInSameScope) {
  BuildGraphAndAddGradientsWithPrefixes("scope/gradients", "scope/gradients_1");
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
}

TEST_F(CApiGradientsTest, GradientsPrefix_TwoGradientsInDifferentScopes) {
  BuildGraphAndAddGradientsWithPrefixes("scope/gradients", "scope_1/gradients");
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
}

TEST_F(CApiGradientsTest, GradientsPrefix_2ndGradientsAsSubScopeOf1st) {
  BuildGraphAndAddGradientsWithPrefixes("gradients", "gradients/sub");
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
}

TEST_F(CApiGradientsTest, GradientsPrefix_PrefixMatchesExistingNodeName) {
  BuildGraphAndAddGradientsWithPrefixes("Const_0");
  ASSERT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s_)) << TF_Message(s_);
}

TEST_F(CApiGradientsTest, GradientsPrefix_TwoGradientsWithIdenticalPrefixes) {
  BuildGraphAndAddGradientsWithPrefixes("gradients", "gradients");
  ASSERT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s_)) << TF_Message(s_);
}

TEST_F(CApiGradientsTest, GradientsPrefix_2ndGradientsMatchingNodeOf1st) {
  BuildGraphAndAddGradientsWithPrefixes("gradients", "gradients/MatMul");
  ASSERT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s_)) << TF_Message(s_);
}

TEST_F(CApiGradientsTest, GradientsPrefix_1stGradientsMatchingNodeOf2nd) {
  BuildGraphAndAddGradientsWithPrefixes("gradients/MatMul", "gradients");
  ASSERT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s_)) << TF_Message(s_);
}

TEST_F(CApiGradientsTest, GradientsPrefix_2ndGradientsAsParentScopeOf1st) {
  BuildGraphAndAddGradientsWithPrefixes("gradients/sub", "gradients");
  ASSERT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s_)) << TF_Message(s_);
}

void ScalarFloatFromTensor(const TF_Tensor* t, float* f) {
  ASSERT_TRUE(t != nullptr);
  ASSERT_EQ(TF_FLOAT, TF_TensorType(t));
  ASSERT_EQ(0, TF_NumDims(t));
  ASSERT_EQ(4, TF_TensorByteSize(t));
  float* p = static_cast<float*>(TF_TensorData(t));
  *f = *p;
}

TEST_F(CApiGradientsTest, MultipleCallsToAddGradients) {
  const float X = 3.0f, Y = 7.0f;
  TF_Operation* x = Placeholder(graph_, s_, "x", TF_FLOAT);
  TF_Operation* y = Placeholder(graph_, s_, "y", TF_FLOAT);
  TF_Operation* xy = Mul(x, y, graph_, s_, "xy");
  TF_Output dxy_dx, dxy_dy;

  TF_Output outputs[1] = {{xy, 0}};
  TF_Output inputs[1] = {{x, 0}};
  TF_AddGradients(graph_, outputs, 1, inputs, 1, nullptr, s_, &dxy_dx);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  inputs[0] = {y, 0};
  TF_AddGradients(graph_, outputs, 1, inputs, 1, nullptr, s_, &dxy_dy);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  TF_SessionOptions* opts = TF_NewSessionOptions();
  TF_Session* sess = TF_NewSession(graph_, opts, s_);
  TF_DeleteSessionOptions(opts);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  TF_Output feeds[] = {{x, 0}, {y, 0}};
  TF_Tensor* feedValues[] = {FloatTensor(X), FloatTensor(Y)};
  TF_Output fetches[] = {dxy_dx, dxy_dy};
  TF_Tensor* fetchValues[] = {nullptr, nullptr};

  TF_SessionRun(sess, nullptr /* run_options */, feeds, feedValues, 2, fetches,
                fetchValues, 2, nullptr /* target_opers */, 0,
                nullptr /* run_metadata */, s_);
  TF_DeleteTensor(feedValues[0]);
  TF_DeleteTensor(feedValues[1]);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  TF_DeleteSession(sess, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  float dxy_dxValue = 0.0f, dxy_dyValue = 0.0f;
  ScalarFloatFromTensor(fetchValues[0], &dxy_dxValue);
  EXPECT_EQ(Y, dxy_dxValue);

  ScalarFloatFromTensor(fetchValues[1], &dxy_dyValue);
  EXPECT_EQ(X, dxy_dyValue);

  TF_DeleteTensor(fetchValues[0]);
  TF_DeleteTensor(fetchValues[1]);
}

// REGISTER_OP for CApiAttributesTest test cases.
// Registers two ops, each with a single attribute called 'v'.
// The attribute in one op will have a type 'type', the other
// will have list(type).
#define ATTR_TEST_REGISTER_OP(type)                           \
  REGISTER_OP("CApiAttributesTestOp" #type)                   \
      .Attr("v: " #type)                                      \
      .SetShapeFn(tensorflow::shape_inference::UnknownShape); \
  REGISTER_OP("CApiAttributesTestOpList" #type)               \
      .Attr("v: list(" #type ")")                             \
      .SetShapeFn(tensorflow::shape_inference::UnknownShape)
ATTR_TEST_REGISTER_OP(string);
ATTR_TEST_REGISTER_OP(int);
ATTR_TEST_REGISTER_OP(float);
ATTR_TEST_REGISTER_OP(bool);
ATTR_TEST_REGISTER_OP(type);
ATTR_TEST_REGISTER_OP(shape);
ATTR_TEST_REGISTER_OP(tensor);
#undef ATTR_TEST_REGISTER_OP

class CApiAttributesTest : public ::testing::Test {
 protected:
  CApiAttributesTest()
      : s_(TF_NewStatus()), graph_(TF_NewGraph()), counter_(0) {}

  ~CApiAttributesTest() override {
    TF_DeleteGraph(graph_);
    TF_DeleteStatus(s_);
  }

  TF_OperationDescription* init(string type) {
    // Construct op_name to match the name used by REGISTER_OP in the
    // ATTR_TEST_REGISTER calls above.
    string op_name = "CApiAttributesTestOp";
    if (type.find("list(") == 0) {
      op_name += "List";
      type = type.replace(0, 5, "");
      type = type.replace(type.size() - 1, 1, "");
    }
    op_name += type;
    return TF_NewOperation(
        graph_, op_name.c_str(),
        ::tensorflow::strings::StrCat("name", counter_++).c_str());
  }

  TF_Status* s_;

 private:
  TF_Graph* graph_;
  int counter_;
};

// Helper macros for the TF_OperationGetAttr* tests.
// TODO(ashankar): Use gmock matchers instead?
// (https://github.com/google/googletest/blob/master/googlemock/docs/CookBook.md#writing-new-parameterized-matchers-quickly)
// That will require setting up the tensorflow build with gmock.
#define EXPECT_TF_META(attr_name, expected_list_size, expected_type, \
                       expected_total_size)                          \
  do {                                                               \
    auto m = TF_OperationGetAttrMetadata(oper, attr_name, s_);       \
    EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);              \
    const unsigned char e = expected_list_size >= 0 ? 1 : 0;         \
    EXPECT_EQ(e, m.is_list);                                         \
    EXPECT_EQ(expected_list_size, m.list_size);                      \
    EXPECT_EQ(expected_type, m.type);                                \
    EXPECT_EQ(expected_total_size, m.total_size);                    \
  } while (0)

TEST_F(CApiAttributesTest, String) {
  auto desc = init("string");
  TF_SetAttrString(desc, "v", "bunny", 5);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TF_META("v", -1, TF_ATTR_STRING, 5);
  std::unique_ptr<char[]> value(new char[5]);

  TF_OperationGetAttrString(oper, "v", value.get(), 5, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_EQ("bunny", string(static_cast<const char*>(value.get()), 5));
}

TEST_F(CApiAttributesTest, StringList) {
  std::vector<string> list = {"bugs", "bunny", "duck"};
  std::unique_ptr<const void*[]> list_ptrs;
  std::unique_ptr<size_t[]> list_lens;
  StringVectorToArrays(list, &list_ptrs, &list_lens);
  int list_total_size = 0;
  for (const auto& s : list) {
    list_total_size += s.size();
  }

  auto desc = init("list(string)");
  TF_SetAttrStringList(desc, "v", list_ptrs.get(), list_lens.get(),
                       list.size());

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  EXPECT_TF_META("v", list.size(), TF_ATTR_STRING, list_total_size);
  std::unique_ptr<void*[]> values(new void*[list.size()]);
  std::unique_ptr<size_t[]> lens(new size_t[list.size()]);
  std::unique_ptr<char[]> storage(new char[list_total_size]);
  TF_OperationGetAttrStringList(oper, "v", values.get(), lens.get(),
                                list.size(), storage.get(), list_total_size,
                                s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  for (size_t i = 0; i < list.size(); ++i) {
    EXPECT_EQ(list[i].size(), lens[i]) << i;
    EXPECT_EQ(list[i], string(static_cast<const char*>(values[i]), lens[i]))
        << i;
  }
}

TEST_F(CApiAttributesTest, Int) {
  auto desc = init("int");
  TF_SetAttrInt(desc, "v", 31415);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TF_META("v", -1, TF_ATTR_INT, -1);

  int64_t value;
  TF_OperationGetAttrInt(oper, "v", &value, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_EQ(31415, value);
}

TEST_F(CApiAttributesTest, IntList) {
  const int64_t list[] = {1, 2, 3, 4};
  const size_t list_size = TF_ARRAYSIZE(list);

  auto desc = init("list(int)");
  TF_SetAttrIntList(desc, "v", list, list_size);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  int64_t values[list_size];
  EXPECT_TF_META("v", list_size, TF_ATTR_INT, -1);
  TF_OperationGetAttrIntList(oper, "v", values, list_size, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TRUE(std::equal(std::begin(list), std::end(list), std::begin(values)));
}

TEST_F(CApiAttributesTest, Float) {
  auto desc = init("float");
  TF_SetAttrFloat(desc, "v", 2.718);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TF_META("v", -1, TF_ATTR_FLOAT, -1);

  float value;
  TF_OperationGetAttrFloat(oper, "v", &value, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_FLOAT_EQ(2.718, value);
}

TEST_F(CApiAttributesTest, FloatList) {
  const float list[] = {1.414, 2.718, 3.1415};
  const size_t list_size = TF_ARRAYSIZE(list);

  auto desc = init("list(float)");
  TF_SetAttrFloatList(desc, "v", list, list_size);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  float values[list_size];
  EXPECT_TF_META("v", list_size, TF_ATTR_FLOAT, -1);
  TF_OperationGetAttrFloatList(oper, "v", values, list_size, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TRUE(std::equal(std::begin(list), std::end(list), std::begin(values)));
}

TEST_F(CApiAttributesTest, Bool) {
  auto desc = init("bool");
  TF_SetAttrBool(desc, "v", 1);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TF_META("v", -1, TF_ATTR_BOOL, -1);

  unsigned char value;
  TF_OperationGetAttrBool(oper, "v", &value, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_EQ(1, value);
}

TEST_F(CApiAttributesTest, BoolList) {
  const unsigned char list[] = {0, 1, 1, 0, 0, 1, 1};
  const size_t list_size = TF_ARRAYSIZE(list);

  auto desc = init("list(bool)");
  TF_SetAttrBoolList(desc, "v", list, list_size);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  unsigned char values[list_size];
  EXPECT_TF_META("v", list_size, TF_ATTR_BOOL, -1);
  TF_OperationGetAttrBoolList(oper, "v", values, list_size, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TRUE(std::equal(std::begin(list), std::end(list), std::begin(values)));
}

TEST_F(CApiAttributesTest, Type) {
  auto desc = init("type");
  TF_SetAttrType(desc, "v", TF_COMPLEX128);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TF_META("v", -1, TF_ATTR_TYPE, -1);

  TF_DataType value;
  TF_OperationGetAttrType(oper, "v", &value, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_EQ(TF_COMPLEX128, value);
}

TEST_F(CApiAttributesTest, TypeList) {
  const TF_DataType list[] = {TF_FLOAT, TF_DOUBLE, TF_HALF, TF_COMPLEX128};
  const size_t list_size = TF_ARRAYSIZE(list);

  auto desc = init("list(type)");
  TF_SetAttrTypeList(desc, "v", list, list_size);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  TF_DataType values[list_size];
  EXPECT_TF_META("v", list_size, TF_ATTR_TYPE, -1);
  TF_OperationGetAttrTypeList(oper, "v", values, list_size, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TRUE(std::equal(std::begin(list), std::end(list), std::begin(values)));
}

TEST_F(CApiAttributesTest, Shape) {
  // Unknown shape
  auto desc = init("shape");
  TF_SetAttrShape(desc, "v", nullptr, -1);
  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TF_META("v", -1, TF_ATTR_SHAPE, -1);
  TF_OperationGetAttrShape(oper, "v", nullptr, 10, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  // Partially specified shape
  const int64_t partial_shape[] = {17, -1};
  const size_t sz = TF_ARRAYSIZE(partial_shape);
  desc = init("shape");
  TF_SetAttrShape(desc, "v", partial_shape, sz);
  oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TF_META("v", -1, TF_ATTR_SHAPE, sz);
  int64_t values[sz];
  TF_OperationGetAttrShape(oper, "v", values, sz, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TRUE(
      std::equal(std::begin(partial_shape), std::end(partial_shape), values));
}

TEST_F(CApiAttributesTest, ShapeList) {
  const int64_t shape_1[] = {1, 3};
  const int64_t shape_2[] = {2, 4, 6};
  const int64_t* list[] = {&shape_1[0], &shape_2[0]};
  const size_t list_size = TF_ARRAYSIZE(list);
  const int ndims[] = {TF_ARRAYSIZE(shape_1), TF_ARRAYSIZE(shape_2)};
  const int total_ndims = 5;  // ndims[0] + ndims[1]

  auto desc = init("list(shape)");
  TF_SetAttrShapeList(desc, "v", list, ndims, list_size);
  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  EXPECT_TF_META("v", list_size, TF_ATTR_SHAPE, total_ndims);
  int64_t* values[list_size];
  int values_ndims[list_size];
  int64_t storage[total_ndims];
  TF_OperationGetAttrShapeList(oper, "v", values, values_ndims, list_size,
                               storage, total_ndims, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  for (size_t i = 0; i < list_size; ++i) {
    EXPECT_EQ(ndims[i], values_ndims[i]) << i;
    for (int j = 0; j < values_ndims[i]; ++j) {
      EXPECT_EQ(list[i][j], values[i][j]) << "(" << i << ", " << j << ")";
    }
  }
}

TEST_F(CApiAttributesTest, TensorShapeProto) {
  const tensorflow::int64 pts[] = {2, 4, -1, 8};
  tensorflow::TensorShapeProto proto;
  tensorflow::PartialTensorShape(pts).AsProto(&proto);
  string bytes;
  proto.SerializeToString(&bytes);

  auto desc = init("shape");
  TF_SetAttrTensorShapeProto(desc, "v", bytes.data(), bytes.length(), s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  EXPECT_TF_META("v", -1, TF_ATTR_SHAPE, 4);
  TF_Buffer* value = TF_NewBuffer();
  TF_OperationGetAttrTensorShapeProto(oper, "v", value, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_EQ(bytes.length(), value->length);
  EXPECT_EQ(0, memcmp(bytes.data(), value->data, value->length));
  TF_DeleteBuffer(value);
}

TEST_F(CApiAttributesTest, TensorShapeProtoList) {
  string bytes1, bytes2;
  tensorflow::TensorShapeProto proto;

  const tensorflow::int64 pts1[] = {2, 4, -1, 8};
  tensorflow::PartialTensorShape(pts1).AsProto(&proto);
  proto.SerializeToString(&bytes1);

  const tensorflow::int64 pts2[] = {1, 3, 5, 7};
  tensorflow::PartialTensorShape(pts2).AsProto(&proto);
  proto.SerializeToString(&bytes2);

  std::unique_ptr<const void*[]> list_ptrs;
  std::unique_ptr<size_t[]> list_lens;
  const std::vector<string> list = {bytes1, bytes2};
  StringVectorToArrays(list, &list_ptrs, &list_lens);

  auto desc = init("list(shape)");
  TF_SetAttrTensorShapeProtoList(desc, "v", list_ptrs.get(), list_lens.get(),
                                 list.size(), s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  EXPECT_TF_META("v", 2, TF_ATTR_SHAPE, 8);
  TF_Buffer* values[2];
  TF_OperationGetAttrTensorShapeProtoList(oper, "v", values, 2, s_);
  EXPECT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  for (int i = 0; i < 2; ++i) {
    int le = list_lens[i];
    int la = values[i]->length;
    const void* e = list_ptrs[i];
    const void* a = values[i]->data;
    EXPECT_EQ(le, la) << i;
    EXPECT_EQ(0, memcmp(e, a, std::min(le, la))) << i;
    TF_DeleteBuffer(values[i]);
  }
}

TEST_F(CApiAttributesTest, Tensor) {
  const char tensor[] = {5, 7};
  const int64_t dims[] = {1, 2};
  const size_t ndims = TF_ARRAYSIZE(dims);

  auto desc = init("tensor");
  unique_tensor_ptr v(Int8Tensor(dims, ndims, tensor), TF_DeleteTensor);
  TF_SetAttrTensor(desc, "v", v.get(), s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  EXPECT_TF_META("v", -1, TF_ATTR_TENSOR, -1);
  TF_Tensor* value;
  TF_OperationGetAttrTensor(oper, "v", &value, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  ASSERT_NE(nullptr, value);
  EXPECT_EQ(TF_INT8, TF_TensorType(value));
  EXPECT_EQ(ndims, TF_NumDims(value));
  for (int i = 0; i < TF_NumDims(value); ++i) {
    EXPECT_EQ(dims[i], TF_Dim(value, i)) << i;
  }
  EXPECT_EQ(sizeof(char) * TF_ARRAYSIZE(tensor), TF_TensorByteSize(value));
  EXPECT_EQ(0, memcmp(tensor, TF_TensorData(value), TF_TensorByteSize(value)));
  TF_DeleteTensor(value);
}

TEST_F(CApiAttributesTest, StringTensor) {
  // Create the string-Tensor "attribute" value.
  char encoded[] = {
      0,   0, 0, 0, 0, 0, 0, 0,  // array[uint64] offsets
      1,                         // varint encoded string length
      'A',
  };
  auto deallocator = [](void* data, size_t len, void* arg) {};
  unique_tensor_ptr t_in(TF_NewTensor(TF_STRING, nullptr, 0, &encoded[0],
                                      sizeof(encoded), deallocator, nullptr),
                         TF_DeleteTensor);

  // Create a TF_Operation with the attribute t_in
  auto desc = init("tensor");
  TF_SetAttrTensor(desc, "v", t_in.get(), s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  // Fetch the attribute back.
  EXPECT_TF_META("v", -1, TF_ATTR_TENSOR, -1);
  TF_Tensor* t_out = nullptr;
  TF_OperationGetAttrTensor(oper, "v", &t_out, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_EQ(TF_STRING, TF_TensorType(t_out));
  EXPECT_EQ(0, TF_NumDims(t_out));
  ASSERT_EQ(TF_TensorByteSize(t_in.get()), TF_TensorByteSize(t_out));
  EXPECT_EQ(0, memcmp(TF_TensorData(t_in.get()), TF_TensorData(t_out),
                      TF_TensorByteSize(t_out)));
  TF_DeleteTensor(t_out);
}

TEST_F(CApiAttributesTest, TensorList) {
  const char tensor1[] = {5, 7};
  const int64_t dims1[] = {1, 2};
  const size_t ndims1 = TF_ARRAYSIZE(dims1);

  const char tensor2[] = {2, 4, 6, 8};
  const int64_t dims2[] = {2, 2};
  const size_t ndims2 = TF_ARRAYSIZE(dims2);

  auto desc = init("list(tensor)");
  TF_Tensor* tmp[] = {
      Int8Tensor(dims1, ndims1, tensor1),
      Int8Tensor(dims2, ndims2, tensor2),
  };
  TF_SetAttrTensorList(desc, "v", tmp, TF_ARRAYSIZE(tmp), s_);
  for (int i = 0; i < TF_ARRAYSIZE(tmp); ++i) {
    TF_DeleteTensor(tmp[i]);
  }
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  EXPECT_TF_META("v", 2, TF_ATTR_TENSOR, -1);
  TF_Tensor* values[2];
  TF_OperationGetAttrTensorList(oper, "v", &values[0], TF_ARRAYSIZE(values),
                                s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);

  const char* tensor_data[] = {&tensor1[0], &tensor2[0]};
  const size_t tensor_size[] = {TF_ARRAYSIZE(tensor1), TF_ARRAYSIZE(tensor2)};
  const int64_t* tensor_dims[] = {&dims1[0], &dims2[0]};
  const size_t tensor_ndims[] = {ndims1, ndims2};
  for (int i = 0; i < 2; ++i) {
    TF_Tensor* v = values[i];
    ASSERT_NE(nullptr, v) << i;
    EXPECT_EQ(TF_INT8, TF_TensorType(v)) << i;
    EXPECT_EQ(tensor_ndims[i], TF_NumDims(v)) << i;
    for (int j = 0; j < TF_NumDims(v); ++j) {
      EXPECT_EQ(tensor_dims[i][j], TF_Dim(v, j))
          << "Tensor #" << i << ", dimension #" << j;
    }
    EXPECT_EQ(sizeof(char) * tensor_size[i], TF_TensorByteSize(v)) << i;
    EXPECT_EQ(0,
              memcmp(tensor_data[i], TF_TensorData(v), TF_TensorByteSize(v)));
    TF_DeleteTensor(v);
  }
}

TEST_F(CApiAttributesTest, EmptyList) {
  auto desc = init("list(int)");
  TF_SetAttrIntList(desc, "v", nullptr, 0);
  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  EXPECT_TF_META("v", 0, TF_ATTR_INT, -1);
}

TEST_F(CApiAttributesTest, Errors) {
  auto desc = init("int");
  TF_SetAttrInt(desc, "v", 3);
  auto oper = TF_FinishOperation(desc, s_);
  ASSERT_EQ(TF_OK, TF_GetCode(s_)) << TF_Message(s_);
  TF_OperationGetAttrString(oper, "v", nullptr, 0, s_);
  EXPECT_EQ(TF_INVALID_ARGUMENT, TF_GetCode(s_)) << TF_Message(s_);
}

TEST(TestApiDef, TestCreateApiDef) {
  // TODO(b/73318067): Fix linking for the GPU test generated by the
  // tf_cuda_cc_test() bazel rule and remove the next line.
  if (!GPUDeviceName().empty()) return;

  TF_Buffer* op_list_buf = TF_GetAllOpList();
  TF_Status* status = TF_NewStatus();
  auto* api_def_map = TF_NewApiDefMap(op_list_buf, status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  TF_DeleteStatus(status);

  string op_name = "TestCApi";
  status = TF_NewStatus();
  auto* api_def_buf =
      TF_ApiDefMapGet(api_def_map, op_name.c_str(), op_name.size(), status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  TF_DeleteStatus(status);

  tensorflow::ApiDef api_def;
  EXPECT_TRUE(api_def.ParseFromArray(api_def_buf->data, api_def_buf->length));
  EXPECT_EQ(op_name, api_def.graph_op_name());
  EXPECT_EQ(R"doc(Used to test C API)doc", api_def.summary());

  TF_DeleteBuffer(api_def_buf);
  TF_DeleteApiDefMap(api_def_map);
  TF_DeleteBuffer(op_list_buf);
}

TEST(TestApiDef, TestCreateApiDefWithOverwrites) {
  // TODO(b/73318067): Fix linking for the GPU test generated by the
  // tf_cuda_cc_test() bazel rule and remove the next line.
  if (!GPUDeviceName().empty()) return;

  TF_Buffer* op_list_buf = TF_GetAllOpList();
  TF_Status* status = TF_NewStatus();
  auto* api_def_map = TF_NewApiDefMap(op_list_buf, status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  TF_DeleteStatus(status);

  string api_def_overwrites = R"(op: <
  graph_op_name: "TestCApi"
  summary: "New summary"
>
)";
  status = TF_NewStatus();
  TF_ApiDefMapPut(api_def_map, api_def_overwrites.c_str(),
                  api_def_overwrites.size(), status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  TF_DeleteStatus(status);

  string op_name = "TestCApi";
  status = TF_NewStatus();
  auto* api_def_buf =
      TF_ApiDefMapGet(api_def_map, op_name.c_str(), op_name.size(), status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  TF_DeleteStatus(status);

  tensorflow::ApiDef api_def;
  EXPECT_TRUE(api_def.ParseFromArray(api_def_buf->data, api_def_buf->length));
  EXPECT_EQ(op_name, api_def.graph_op_name());
  EXPECT_EQ("New summary", api_def.summary());

  TF_DeleteBuffer(api_def_buf);
  TF_DeleteApiDefMap(api_def_map);
  TF_DeleteBuffer(op_list_buf);
}

class DummyKernel : public tensorflow::OpKernel {
 public:
  explicit DummyKernel(tensorflow::OpKernelConstruction* context)
      : OpKernel(context) {}
  void Compute(tensorflow::OpKernelContext* context) override {}
};

// Test we can query kernels
REGISTER_OP("TestOpWithSingleKernel")
    .Input("a: float")
    .Input("b: float")
    .Output("o: float");
REGISTER_KERNEL_BUILDER(
    Name("TestOpWithSingleKernel").Device(tensorflow::DEVICE_CPU), DummyKernel);

TEST(TestKernel, TestGetAllRegisteredKernels) {
  TF_Status* status = TF_NewStatus();
  TF_Buffer* kernel_list_buf = TF_GetAllRegisteredKernels(status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  KernelList kernel_list;
  kernel_list.ParseFromArray(kernel_list_buf->data, kernel_list_buf->length);
  ASSERT_GT(kernel_list.kernel_size(), 0);
  TF_DeleteBuffer(kernel_list_buf);
  TF_DeleteStatus(status);
}

TEST(TestKernel, TestGetRegisteredKernelsForOp) {
  TF_Status* status = TF_NewStatus();
  TF_Buffer* kernel_list_buf =
      TF_GetRegisteredKernelsForOp("TestOpWithSingleKernel", status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  KernelList kernel_list;
  kernel_list.ParseFromArray(kernel_list_buf->data, kernel_list_buf->length);
  ASSERT_EQ(kernel_list.kernel_size(), 1);
  EXPECT_EQ(kernel_list.kernel(0).op(), "TestOpWithSingleKernel");
  EXPECT_EQ(kernel_list.kernel(0).device_type(), "CPU");
  TF_DeleteBuffer(kernel_list_buf);
  TF_DeleteStatus(status);
}

TEST(TestKernel, TestGetRegisteredKernelsForOpNoKernels) {
  TF_Status* status = TF_NewStatus();
  TF_Buffer* kernel_list_buf = TF_GetRegisteredKernelsForOp("Unknown", status);
  EXPECT_EQ(TF_OK, TF_GetCode(status)) << TF_Message(status);
  KernelList kernel_list;
  kernel_list.ParseFromArray(kernel_list_buf->data, kernel_list_buf->length);
  ASSERT_EQ(kernel_list.kernel_size(), 0);
  TF_DeleteBuffer(kernel_list_buf);
  TF_DeleteStatus(status);
}

#undef EXPECT_TF_META

TEST(CAPI, TestTensorAligned) {
  int64_t dim = 7;
  size_t tensor_size_bytes = dim * TF_DataTypeSize(TF_FLOAT);
  TF_Tensor* a = TF_AllocateTensor(
      /*dtype=*/TF_FLOAT, /*dims=*/&dim, /*num_dims=*/1,
      /*len=*/tensor_size_bytes);
  float* data = reinterpret_cast<float*>(TF_TensorData(a));
  for (int i = 0; i < dim; ++i) {
    data[i] = 0;
  }
  if (EIGEN_MAX_ALIGN_BYTES > 0) {
    EXPECT_TRUE(TF_TensorIsAligned(a));
  }
  TF_DeleteTensor(a);
}

TEST(CAPI, TestTensorIsNotAligned) {
  // Test unaligned access via a Slice.
  Tensor x(DT_FLOAT, TensorShape({30}));
  x.flat<float>().setConstant(0.0);

  // Take an unaligned slice.
  Tensor y = x.Slice(1, 13);
  TF_Status* status = TF_NewStatus();
  TF_Tensor* a = TF_TensorFromTensor(y, status);
  if (EIGEN_MAX_ALIGN_BYTES > 0) {
    EXPECT_FALSE(TF_TensorIsAligned(a));
  }
  TF_DeleteStatus(status);
  TF_DeleteTensor(a);
}

}  // namespace
}  // namespace tensorflow

// TODO(josh11b): Test:
// * TF_SetDevice(desc, "/job:worker");
// * control inputs / outputs
// * targets
// * TF_DeleteGraph() before TF_DeleteSession()
